<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>SEO · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/seo/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/seo/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Làm sao để được ChatGPT trích dẫn: những gì log của một blog nhỏ cho thấy</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/</guid>
      <pubDate>Wed, 16 Sep 2026 00:00:00 +1200</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>SEO</category>
      <category>Crawlers</category>
      <category>LLM</category>
      <category>Hugo</category>
      <category>Cloudflare</category>
      <category>content strategy</category>
      <category>Writing</category>
      <description>Muốn được ChatGPT và các trợ lý AI trích dẫn (GEO): nên cho bot nào vào, HTML không cần trình duyệt, tên tác giả trong dữ liệu có cấu trúc, và llms.txt mà chẳng ai đọc.</description>
      <content:encoded><![CDATA[<p>Muốn được ChatGPT và các trợ lý AI khác trích dẫn, việc đầu tiên là cho bot tìm kiếm của chúng, loại bot tải trang ngay lúc trả lời, vào đọc trang của bạn. Chúng khác với các crawler huấn luyện, và chặn loại này chẳng ảnh hưởng gì đến loại kia. Sau đó là trả về HTML đọc được mà không cần chạy JavaScript, kèm dữ liệu có cấu trúc (structured data) ghi rõ tên tác giả. Còn llms.txt thì theo log của tôi chẳng ai đến lấy nên không có tác dụng gì; điều quan trọng nhất là viết sao cho mỗi phần nêu luận điểm ngay câu đầu, để một đoạn có thể được trích dẫn riêng.</p>
<p>Lượng truy cập từ công cụ tìm kiếm vào blog này đã đứng yên vài năm nay, và tôi cũng thôi mong nó nhúc nhích. Nên thay vào đó tôi đi xem khách đến từ đâu, và trong số đó có các trợ lý AI (AI assistant) hay không. Có, nhưng chỉ vừa đủ để gọi là có. Trong mười hai tháng tính đến ngày 16 tháng 9 năm 2026, công cụ phân tích truy cập của tôi đếm được 5,372 phiên truy cập (session) ở đây. Hai mươi bảy phiên trong số đó đến từ một trợ lý AI: mười hai từ chatgpt.com, chín từ gemini.google.com, sáu từ Perplexity, và không có phiên nào từ claude.ai. Nửa phần trăm. Nhưng khách đến từ ChatGPT lại là loại khách tốt. Họ ở lại trung bình năm phút, so với 27 giây của lượt truy cập trực tiếp và ba phút của khách từ Google, vì họ đã đọc xong câu trả lời rồi và muốn xem chi tiết.</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="Số phiên theo nguồn giới thiệu trong mười hai tháng, trợ lý AI được tô đậm"
    width="2400"
    height="1164"
    src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_590f408ed875db1f.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_590f408ed875db1f.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_d8da7a3f1d1c9190.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-referrers_hu_b3dda7d23abf271d.webp"></span><figcaption>Số phiên theo nguồn trong mười hai tháng. Các cột xanh là trợ lý AI: 27 trên tổng 5,372 phiên.</figcaption></figure>
</p>
<p>Nên tôi đã dành thời gian tìm hiểu xem một blog nhỏ làm thế nào để được chọn làm nguồn cho những câu trả lời đó. Bài này là những gì tôi đã làm trên trang này, cùng với những gì log nói về chuyện đó. Chỗ nào có số thì tôi đưa số, chỗ nào đang đoán thì tôi nói rõ là đoán. Có ba câu trong bản nháp đầu hóa ra sai khi tôi thực sự nhìn vào số liệu, và tôi để nguyên các chỗ sửa trong bài.</p>

<h2 class="relative group">Hai loại bot, và chỉ một loại trích dẫn bạn
    <div id="hai-loại-bot-và-chỉ-một-loại-trích-dẫn-bạn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#hai-lo%e1%ba%a1i-bot-v%c3%a0-ch%e1%bb%89-m%e1%bb%99t-lo%e1%ba%a1i-tr%c3%adch-d%e1%ba%abn-b%e1%ba%a1n" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Điều đầu tiên cần phân định rõ là &ldquo;bot AI&rdquo; thực ra là hai công việc khác nhau với hai bộ user agent khác nhau, và chặn loại này chẳng ảnh hưởng gì đến loại kia.</p>
<p>Trình thu thập dữ liệu (crawler) huấn luyện kéo trang về hàng loạt để xây mô hình tiếp theo. Của OpenAI là GPTBot, của Anthropic là ClaudeBot, của Google được điều khiển qua token Google-Extended, của Apple qua Applebot-Extended, còn CCBot của Common Crawl thì cấp dữ liệu cho phần lớn các bộ dữ liệu mở. Những gì chúng lấy đi cuối cùng trở thành trọng số (weights) của mô hình, không có đường dẫn nào trỏ ngược về bạn. Chặn những bot này là có lý, và tôi không cho đó là chuyện vớ vẩn.</p>
<p>Bot truy xuất (retrieval bot) là loại còn lại. Khi ai đó hỏi ChatGPT, Claude hay Perplexity một câu, trợ lý sẽ chạy một lượt tìm kiếm, tải về ngay lúc đó một nhúm trang, đọc chúng, rồi viết câu trả lời kèm trích dẫn (citation). Những lượt tải đó đến dưới tên OAI-SearchBot và ChatGPT-User, Claude-SearchBot và Claude-User, PerplexityBot và Perplexity-User. AI Overviews của Google thì dùng Googlebot thường, nên chẳng có công tắc riêng nào cho chúng cả. Đây mới là những bot đưa người đọc quay lại với bạn. Nếu mục tiêu là được trích dẫn thì đây là những bot bạn đang tối ưu cho, và cũng nên biết rằng chặn GPTBot không ảnh hưởng gì đến kết quả tìm kiếm của ChatGPT, theo cả hai chiều.</p>
<p>robots.txt của trang này là bản mặc định của Hugo, tức là cho phép tất cả:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">User-agent: *
</span></span><span class="line"><span class="cl">Allow: /
</span></span><span class="line"><span class="cl">Sitemap: https://jared.lynskey.co.nz/sitemap.xml</span></span></code></pre></div></div>
<p>Tôi đã cân nhắc tách nó ra, cho bot truy xuất vào và giữ bot huấn luyện ở ngoài, rồi quyết định là không đáng làm với một blog mà mục đích duy nhất là để được đọc. Nếu tôi muốn vậy thì nó sẽ trông như khối bên dưới. Lý do đưa nó ra đây cũng là bài học trong bài <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-09-09-cloudflare-crawlers-cache/" >Cloudflare trả về cho Googlebot một trang trống</a>: danh sách phải gọi tên bot, không phải tên hãng. Không có dòng nào bạn viết ra được để nói &ldquo;OpenAI&rdquo;.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">User-agent: GPTBot
</span></span><span class="line"><span class="cl">User-agent: ClaudeBot
</span></span><span class="line"><span class="cl">User-agent: Google-Extended
</span></span><span class="line"><span class="cl">User-agent: Applebot-Extended
</span></span><span class="line"><span class="cl">User-agent: CCBot
</span></span><span class="line"><span class="cl">Disallow: /
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">User-agent: *
</span></span><span class="line"><span class="cl">Allow: /</span></span></code></pre></div></div>
<p>Một điều cần kiểm tra nếu trang nằm sau Cloudflare. Từ giữa năm 2025, các zone mới ở đó chặn trình thu thập dữ liệu AI theo mặc định, và có một nút bật một chạm trong phần cài đặt bot làm điều tương tự với các zone cũ. Nó chặn cả hai loại. Blog này vừa chuyển sang Cloudflare Pages tuần trước, nên việc đó có trong danh sách của tôi. Nếu bạn bật nó lên vì &ldquo;bot AI&rdquo; nghe như thứ nên chặn ở ngoài, thì bạn cũng vừa tắt luôn việc được trích dẫn. Cloudflare sau đó đã bắt đầu ghi một dòng <code>Content-Signal</code> vào các tệp robots.txt mà họ quản lý, với ba cờ riêng <code>search</code>, <code>ai-input</code> và <code>ai-train</code>. Không ai bị bắt buộc phải tuân theo, nhưng đó là đúng bộ từ vựng: cùng cách phân chia như trên, diễn đạt như một nguyện vọng thay vì một bức tường.</p>

<h2 class="relative group">Thực sự thì ai đang gõ cửa
    <div id="thực-sự-thì-ai-đang-gõ-cửa" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#th%e1%bb%b1c-s%e1%bb%b1-th%c3%ac-ai-%c4%91ang-g%c3%b5-c%e1%bb%ada" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Việc chuyển sang Cloudflare có một tác dụng phụ: lần đầu tiên tôi nhìn thấy được từng request theo user agent. Dưới đây là những gì đã ghé vào trong bốn ngày từ ngày 13 đến ngày 16 tháng 9 năm 2026, sau khi đã loại bỏ các bot giả mạo (spoof), phần này tôi sẽ nói ngay sau đây.</p>
<table>
	<thead>
			<tr>
					<th>Bot</th>
					<th>Số request</th>
					<th>Nó đã làm gì</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>ClaudeBot (huấn luyện)</td>
					<td>1,777</td>
					<td>1,380 trang khác nhau trong hai ngày, đủ cả năm ngôn ngữ, cộng thêm 37 lần đọc robots.txt</td>
			</tr>
			<tr>
					<td>meta-externalagent (huấn luyện)</td>
					<td>1,748</td>
					<td>Y hệt như trên, chậm hơn một ngày</td>
			</tr>
			<tr>
					<td>OAI-SearchBot (chỉ mục tìm kiếm của ChatGPT)</td>
					<td>59</td>
					<td>Các trang ở cả năm ngôn ngữ, và robots.txt mười sáu lần</td>
			</tr>
			<tr>
					<td>ChatGPT-User (tải về lúc đang trả lời)</td>
					<td>20</td>
					<td>Bài tiếng Nhật về khởi động nguội (cold start), bài về máy chủ cập nhật tự dựng, trang chủ</td>
			</tr>
			<tr>
					<td>Claude-User (tải về lúc đang trả lời)</td>
					<td>8</td>
					<td>Bài về máy chủ cập nhật, trang liên hệ, trang chủ</td>
			</tr>
			<tr>
					<td>PerplexityBot</td>
					<td>2</td>
					<td>Bài về gỡ lỗi (debug) iOS, bản tiếng Anh và bản tiếng Việt</td>
			</tr>
			<tr>
					<td>Claude-SearchBot, Perplexity-User</td>
					<td>0</td>
					<td></td>
			</tr>
			<tr>
					<td>Googlebot / Bingbot / Applebot / CCBot</td>
					<td>168 / 71 / 45 / 28</td>
					<td></td>
			</tr>
	</tbody>
</table>
<p>Có hai điều nổi bật. Các bot huấn luyện (training crawler) đọc hết mọi thứ, tới hai lần, trong khoảng thời gian mà các bot truy xuất chỉ đọc vài chục trang. Và bot truy xuất không tải về một cách ngẫu nhiên: cả ChatGPT-User lẫn Claude-User đều nhắm vào bài về máy chủ cập nhật, là bài cụ thể nhất trên trang này, còn bài về khởi động nguội bản tiếng Nhật thì được tải về đúng bằng tiếng Nhật.</p>
<p>Giờ đến phần giả mạo. Trong 20 request tự xưng là GPTBot, có 18 đến từ những địa chỉ nằm ngoài dải địa chỉ IP (IP range) mà OpenAI công bố, và chúng đòi <code>/.env</code>, <code>/.git/HEAD</code> và <code>/firebase-adminsdk.json</code>. Mười lăm trong 36 request &ldquo;ChatGPT-User&rdquo; cũng là kiểu đó. Phần lớn lưu lượng &ldquo;PerplexityBot&rdquo; thì muốn lấy <code>/id_ed25519</code>. User agent chỉ là một chuỗi ký tự mà ai cũng gửi được, và các công cụ quét lỗ hổng đã nhận ra rằng cứ mang tên bot AI là được cho qua. OpenAI, Anthropic và Perplexity đều công bố dải địa chỉ IP của mình. Nếu bạn định đối xử đặc biệt với một bot, dù là cho vào hay chặn lại, hãy kiểm tra địa chỉ chứ đừng tin cái tên. Các con số 20, 59 và 8 ở trên là những request đến từ đúng địa chỉ.</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="Các yêu cầu tự nhận là user agent của OpenAI, tách theo việc có đến từ dải IP OpenAI công bố hay không"
    width="2400"
    height="722"
    src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_e7035266dc7ce0b1.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_e7035266dc7ce0b1.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_638e8c6286f5c91f.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-spoofed_hu_11d44262308d7c6c.webp"></span><figcaption>Cùng một chuỗi user agent, tách theo địa chỉ nguồn. Phần lớn lưu lượng tự xưng &lsquo;GPTBot&rsquo; không hề đến từ OpenAI.</figcaption></figure>
</p>

<h2 class="relative group">Trang phải đọc được mà không cần trình duyệt
    <div id="trang-phải-đọc-được-mà-không-cần-trình-duyệt" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#trang-ph%e1%ba%a3i-%c4%91%e1%bb%8dc-%c4%91%c6%b0%e1%bb%a3c-m%c3%a0-kh%c3%b4ng-c%e1%ba%a7n-tr%c3%acnh-duy%e1%bb%87t" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Bot truy xuất tải trang ngay lúc trả lời, có giới hạn thời gian chờ (timeout), và phần lớn không chạy JavaScript. Một trang chỉ là cái div rỗng cho đến khi bundle tải xong là một trang chúng không đọc. Hugo xuất ra HTML hoàn chỉnh, nên blog này có sẵn điều đó mà không phải làm gì, và đó là lý do chính tôi chưa bao giờ bị cám dỗ chuyển nó sang thứ gì đó thông minh hơn. Sàn mua bán tôi đang vận hành là trường hợp ngược lại, một ứng dụng web React Native, và để trình thu thập dữ liệu nhận được một tài liệu thực sự ở đó, tôi đã phải làm một nhánh dựng trang động (dynamic rendering), rồi dính một lỗi bộ nhớ đệm (cache) lặng lẽ phá hỏng nó. Bài Cloudflare ở trên chính là câu chuyện đó.</p>
<p>Tốc độ cũng được tính theo cách tương tự, và ở chỗ này log đã sửa lưng tôi. Tôi vẫn đinh ninh một trang tĩnh nằm sau Cloudflare thì được phục vụ từ máy chủ biên (edge) gần nhất. Không phải vậy. Blog này chạy trên Cloudflare Pages, HTML được trả ra mà không qua bộ nhớ đệm, và từ Seoul thì phản hồi quay về từ Los Angeles mất khoảng nửa giây. Với một bot có thời gian chờ vài giây thì vẫn ổn, nhưng nó không phải vài chục mili giây như tôi suýt viết ra, và tôi biết được chỉ vì đã đo. Một trợ lý đang ghép câu trả lời từ sáu trang sẽ bỏ cái trang mất ba giây, và nó không nói cho bạn biết là nó đã bỏ.</p>

<h2 class="relative group">Viết sao cho nhấc nguyên một đoạn ra được
    <div id="viết-sao-cho-nhấc-nguyên-một-đoạn-ra-được" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#vi%e1%ba%bft-sao-cho-nh%e1%ba%a5c-nguy%c3%aan-m%e1%bb%99t-%c4%91o%e1%ba%a1n-ra-%c4%91%c6%b0%e1%bb%a3c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Cách các hệ thống này dùng một trang không giống cách một người đọc dùng. Chúng cắt trang thành từng khúc (chunk), chấm điểm từng khúc so với câu hỏi, rồi trích những khúc điểm cao. Một bài dẫn dắt suốt tám trăm chữ mới đến ý chính thì khúc nào điểm cũng thấp. Một bài mà mỗi phần nêu luận điểm ngay câu đầu rồi mới chứng minh thì phần khớp với câu hỏi sẽ ghi điểm.</p>
<p>Điều đó thay đổi cách tôi viết ở đây nhiều hơn bất cứ thứ gì thuộc về kỹ thuật. Phần description trong front matter giờ là tóm tắt bài nói gì, không phải câu mồi để bạn bấm vào đọc, vì description thường là phần duy nhất trợ lý hiển thị. Tiêu đề mục nói thẳng mục đó kết luận gì. Con số đi kèm đơn vị ngay trong câu thay vì nằm trong biểu đồ. Ngày tháng được viết ra đầy đủ, vì &ldquo;tuần trước&rdquo; chẳng có nghĩa gì với một mô hình đọc trang này một năm sau. Và một bài chỉ nói về một chuyện.</p>
<p>Nửa còn lại là phải có thứ đáng để trích, và ở chỗ này số liệu lại không thuận một chiều. Trong log, những trang mà bot tải về lúc trả lời là những bài cụ thể: khởi động nguội, máy chủ cập nhật, gỡ lỗi iOS. Trình thu thập dữ liệu của Naver tên là Yeti và của Daum là Daumoa, một lần khởi động lại máy ở nhà cung cấp hosting của tôi là một khoảng ba mươi giây trả lỗi 504, Cloudflare nén Brotli tại chỗ đo trên bundle của tôi còn tệ hơn gzip. Những dữ kiện đó không được ghi lại ở nhiều nơi, nên câu hỏi nào cần đến một trong số chúng sẽ rơi vào đây. Nhưng những người bấm từ trợ lý sang thì phần lớn lại rơi vào các bài về quản lý từ năm 2023 và 2024, mỗi bài một hai phiên. Bài cụ thể thì máy đọc. Bài chung chung thì người bấm. Tôi không có lời giải thích gọn gàng nào cho chuyện đó, và tôi thà kể lại đúng như vậy còn hơn bịa ra một lý do. Đòn bẩy, nếu có, vẫn là viết ra cái điều cụ thể bạn vừa phát hiện tuần này mà lúc tìm kiếm bạn không thấy đâu, vì đó rõ ràng là thứ máy đang đọc.</p>

<h2 class="relative group">Nói rõ ai viết, và lần nào cũng nói như nhau
    <div id="nói-rõ-ai-viết-và-lần-nào-cũng-nói-như-nhau" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#n%c3%b3i-r%c3%b5-ai-vi%e1%ba%bft-v%c3%a0-l%e1%ba%a7n-n%c3%a0o-c%c5%a9ng-n%c3%b3i-nh%c6%b0-nhau" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Một trợ lý khi cân nhắc có trích dẫn một trang hay không đang làm gần giống một người đọc cẩn thận: ai viết cái này, viết khi nào, và nó có khớp với mọi thứ khác tôi đã tìm thấy không? Phần cơ chế của việc đó trên trang này chủ yếu là do theme làm. Mỗi bài đều mang dữ liệu có cấu trúc (structured data) kiểu Article với tác giả, ngày đăng và ngày sửa, và tác giả là một Person với liên kết đến cùng các hồ sơ công khai. Đó là vài dòng trong template, và là khác biệt giữa &ldquo;một trang&rdquo; với &ldquo;một trang của một người có tên tuổi và có quá trình&rdquo;.</p>
<p>Phần không phải theme làm là sự nhất quán. Trang giới thiệu, llms.txt, các hồ sơ công khai của tôi và dòng ở đầu mỗi bài đều mô tả cùng một người với cùng một quá trình làm việc. Khi có gì thay đổi thì tất cả cùng đổi, nếu không, cái tôi quên sẽ thành cái sai. Nghe có vẻ tỉ mẩn, nhưng một trang nói tôi là quản lý kỹ thuật (engineering manager) còn trang khác nói tôi làm tự do chính là kiểu mâu thuẫn khiến mô hình trả lời nước đôi, hoặc chọn người khác.</p>
<p>Ngày sửa cũng phải thành thật. Nó đổi khi nội dung đổi, ngoài ra tôi không đụng vào. Đẩy ngày lên cho trông có vẻ mới là kiểu chiêu dùng được một lần, rồi cả tên miền của bạn bị hạ tín nhiệm.</p>

<h2 class="relative group">llms.txt là gì, và sự thật là chẳng ai đọc nó
    <div id="llmstxt-là-gì-và-sự-thật-là-chẳng-ai-đọc-nó" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#llmstxt-l%c3%a0-g%c3%ac-v%c3%a0-s%e1%bb%b1-th%e1%ba%adt-l%c3%a0-ch%e1%ba%b3ng-ai-%c4%91%e1%bb%8dc-n%c3%b3" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>llms.txt là một đề xuất từ tháng Chín năm 2024 về một tệp Markdown đặt ở gốc trang, cung cấp cho mô hình ngôn ngữ một bản tóm tắt về trang cùng một danh sách liên kết chọn lọc. Tôi thêm một tệp như vậy vào tháng Hai năm nay. Nó gồm một đoạn về tôi là ai, rồi các mục cho quản lý, hạ tầng, di động và cộng đồng, mỗi bài một dòng: tiêu đề, URL, và một câu nói bài đó kết luận gì. Đọc nó giống như một mục lục viết cho người đang vội.</p>
<p>Điều đáng nói là chẳng ai đọc nó. Trong bốn ngày log tôi có, những request duy nhất tới <code>/llms.txt</code> là của chính tôi. Common Crawl đã lấy 76 trang của site này trong đợt thu thập tháng 8 năm 2026 mà chưa lần nào đụng đến tệp đó. Chưa có trợ lý lớn nào nói rằng họ đọc nó. Nên nó không phải lý do bất cứ thứ gì ở đây được trích dẫn, và tôi sẽ không khuyên ai làm nó vì lý do đó. Tôi giữ nó vì hai lý do. Nó tốn một tiếng, và việc viết cho mỗi bài một câu nói bài đó thực sự kết luận gì còn đáng giá hơn bản thân cái tệp, vì nó cho tôi thấy bài nào chẳng kết luận được gì. Nó cũng là trang duy nhất trên site nói cho máy biết trang này để làm gì, và tôi thà có mà không cần đến. Từ đó nó đã lệch đi, vì tôi viết tay và hay quên, và đó là lý do nên sinh nó từ front matter như một định dạng đầu ra (output format) của Hugo thay vì viết tay. Việc đó đang nằm trong danh sách.</p>

<h2 class="relative group">Feed, sitemap và năm ngôn ngữ
    <div id="feed-sitemap-và-năm-ngôn-ngữ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#feed-sitemap-v%c3%a0-n%c4%83m-ng%c3%b4n-ng%e1%bb%af" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Phần đường ống nhàm chán vẫn có giá trị. Sitemap là cách trình thu thập dữ liệu tìm thấy bài mới ngay ngày nó lên. RSS là cách nó biết một bài đã thay đổi. Cả hai đều là mặc định của Hugo và tôi chưa bao giờ phải nghĩ đến. ClaudeBot đọc sitemap 36 lần trong hai ngày, và đó là cách nó tìm ra 1,380 trang nhanh đến vậy.</p>
<p>Ngôn ngữ là phần tôi không ngờ lại quan trọng. Mọi thứ ở đây có bản tiếng Anh, Nhật, Hàn, Trung giản thể và Việt, với các liên kết thay thế (alternate link) để trình thu thập dữ liệu biết chúng là cùng một bài. Các trình thu thập dữ liệu coi chúng như năm site riêng: lượt thu thập của ClaudeBot chia gần như đều cho cả năm, và trong 73 trang bài viết mà Common Crawl lấy hồi tháng 8, chỉ có bốn trang là bản tiếng Anh. Trợ lý trả lời bằng ngôn ngữ được hỏi và nghiêng về nguồn bằng chính ngôn ngữ đó, nên một câu hỏi tiếng Hàn về khởi động nguội có thể được trả lời từ trang tiếng Hàn thay vì từ một bản dịch tại chỗ của trang tiếng Anh, và trang tiếng Nhật chính là trang mà ChatGPT thực sự đã tải về. Điều kiện là bản dịch phải đọc như thể được viết bằng chính ngôn ngữ đó. Lượt rà soát tôi làm đầu năm nay, có người bản ngữ kiểm tra, là dành cho người đọc, nhưng nó cũng là thứ khiến trang tiếng Việt trở thành một nguồn thay vì một món lạ.</p>

<h2 class="relative group">Những gì tôi đo được
    <div id="những-gì-tôi-đo-được" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-g%c3%ac-t%c3%b4i-%c4%91o-%c4%91%c6%b0%e1%bb%a3c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Nhiều hơn tôi tưởng, một khi tôi thôi đoán mò. Gói miễn phí của Cloudflare cho xem log request theo user agent và đường dẫn, mỗi lần truy vấn được một ngày, và bảng bot ở trên là từ đó mà ra; còn muốn xem referrer thì phải lên gói trả phí. Công cụ phân tích truy cập thì có referrer, và kéo chúng ra qua API báo cáo của nó thay vì căng mắt nhìn dashboard chính là cách tôi có được các con số ở đầu bài. Ghép hai nguồn lại thì thấy được hình dạng của câu chuyện. Trong bốn ngày, các bot truy xuất tải về khoảng chín mươi trang từ các địa chỉ của OpenAI và Anthropic. Trong tháng vừa rồi, có hai người đến từ ChatGPT. Đọc nhiều, thỉnh thoảng trích dẫn, hiếm khi bấm vào. Vẫn chưa có cách nào tôi biết để xem những câu trả lời đã dùng một trang mà không được bấm vào. Cách kiểm tra hữu ích nhất vẫn là làm bằng tay: hỏi trợ lý đúng câu hỏi mà bài của bạn trả lời, rồi xem nó trích trang của ai.</p>
<p><figure><span class="mc-img"><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="auto"
    alt="Số phiên do trợ lý AI giới thiệu theo tháng trong mười ba tháng, không có xu hướng"
    width="2400"
    height="968"
    src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_cd5dc99be8b0cfc3.webp"
    srcset="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_cd5dc99be8b0cfc3.webp 800w, https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_7d401a84f177509c.webp 1280w"
    sizes="(min-width: 768px) 50vw, 65vw"
    data-zoom-src="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/chart-monthly_hu_5c308d89ad110ef7.webp"></span><figcaption>Lượt truy cập từ trợ lý AI theo tháng. Không có xu hướng gì đáng nói.</figcaption></figure>
</p>
<p>Nếu phải giữ lại một thứ từ tất cả những điều này thì đó là quy tắc viết. robots.txt, dữ liệu có cấu trúc và llms.txt là công việc của một buổi chiều, xong là xong. Viết sao cho mỗi phần nêu luận điểm trước, và viết về điều cụ thể thay vì điều chung chung, là một thói quen, và chính thói quen đó quyết định một trang có đáng trích dẫn hay không. Điều thứ hai là hãy đo trước rồi hãy khẳng định. Hóa ra được máy tin cậy trông rất giống được người tin cậy: nói bạn là ai, nói bạn biết gì, và lần nào cũng nói theo cùng một cách.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>SEO cho React Native Web: khi Cloudflare trả về cho Googlebot một trang trống</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/2026-09-09-cloudflare-crawlers-cache/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/2026-09-09-cloudflare-crawlers-cache/</guid>
      <pubDate>Wed, 09 Sep 2026 04:00:00 +1200</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Cloudflare</category>
      <category>SEO</category>
      <category>Django</category>
      <category>Caching</category>
      <category>Crawlers</category>
      <category>Naver</category>
      <category>React Native</category>
      <description>Cách tôi làm cho ứng dụng React Native Web hiện ra với Google và trình thu thập dữ liệu của Naver bằng kết xuất động, và quy tắc bộ nhớ đệm Cloudflare đã trả cho Googlebot khung SPA rỗng.</description>
      <content:encoded><![CDATA[<p>Nếu trang web React hay React Native Web của bạn là một ứng dụng trang đơn, trình thu thập dữ liệu (crawler) của Naver (Yeti) và Bing sẽ không chạy JavaScript của nó, còn Google thì chỉ thỉnh thoảng mới chạy. Cách sửa là kết xuất động (dynamic rendering): nhận diện crawler qua user-agent rồi trả cho nó HTML kết xuất sẵn ở máy chủ, với đúng nội dung mà ứng dụng sẽ vẽ ra. Sai lầm cần tránh trên Cloudflare là lưu đệm URL đó cho tất cả mọi người. Cloudflare bỏ qua <code>Vary</code> với HTML, nên nội dung nào đến trước sẽ được trả cho mọi lượt truy cập sau, và Googlebot có thể nhận về cái khung rỗng; chỉ lưu đệm request của crawler, và xóa bộ nhớ đệm (purge) sau khi thu hẹp quy tắc.</p>
<p>Ứng dụng chợ (marketplace) mà tôi đang làm là một ứng dụng trang đơn (single-page app) viết bằng React Native Web. Trước khi JavaScript chạy, trang tin đăng (listing) nào cũng chỉ có đúng một thẻ script và một thẻ div rỗng. Với người dùng thì chẳng sao, nhưng với công cụ tìm kiếm thì coi như xong: Google thỉnh thoảng chạy được JS, Bing thì không, còn trình thu thập dữ liệu (crawler) của Naver — thứ quan trọng nhất với một trang web mà gần như toàn bộ tin đăng đều bằng tiếng Hàn — thì chắc chắn là không.</p>
<p>Bài này kể lại quá trình tôi làm cho các trang đó hiện ra với công cụ tìm kiếm, rồi chuyện một quy tắc lưu đệm (cache rule) phá hỏng tất cả theo cách tôi không hề lường trước.</p>

<h2 class="relative group">Kết xuất động, chứ không phải che giấu nội dung
    <div id="kết-xuất-động-chứ-không-phải-che-giấu-nội-dung" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#k%e1%ba%bft-xu%e1%ba%a5t-%c4%91%e1%bb%99ng-ch%e1%bb%a9-kh%c3%b4ng-ph%e1%ba%a3i-che-gi%e1%ba%a5u-n%e1%bb%99i-dung" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Cách sửa thì chẳng có gì mới, và được chính Google chấp nhận dưới cái tên kết xuất động (dynamic rendering), khác hẳn với che giấu nội dung (cloaking): nhận diện trình thu thập dữ liệu, rồi trả cho nó HTML đã kết xuất (render) sẵn ở máy chủ, chứa đúng nội dung mà SPA lẽ ra sẽ vẽ lên màn hình. Không phải nội dung khác. Vẫn nội dung ấy, chỉ là có sớm hơn.</p>
<p>Vì vậy, một URL tin đăng như <code>/listings/view?id=...</code> được tách làm hai nhánh. Trình duyệt nhận phần khung (shell) của SPA. Trình thu thập dữ liệu nhận một tài liệu HTML thật sự: tiêu đề, các thông tin chính của tin đăng, mỗi ảnh một thẻ ảnh kèm văn bản thay thế (alt text), một đoạn trích nội dung, và liên kết sang vài tin đăng liên quan để trang không bị mồ côi.</p>
<p>Phần cuối này hoá ra quan trọng hơn tôi nghĩ. Trước khi có mấy liên kết nội bộ đó, khoảng một nghìn trang tin đăng nằm trơ trọi, không có liên kết nào trỏ tới, và Search Console cho thấy lượng thu thập chững lại ở mức bảy đến mười một yêu cầu mỗi ngày. Trình thu thập dữ liệu mà không tìm thấy trang của bạn thì trang có hay đến mấy cũng vô nghĩa.</p>

<h2 class="relative group">Cái bẫy: tên công ty không phải là tên bot
    <div id="cái-bẫy-tên-công-ty-không-phải-là-tên-bot" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1i-b%e1%ba%aby-t%c3%aan-c%c3%b4ng-ty-kh%c3%b4ng-ph%e1%ba%a3i-l%c3%a0-t%c3%aan-bot" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Nhận diện trình thu thập dữ liệu thực chất là so khớp chuỗi user-agent, và tôi dính đòn đúng ở bước này.</p>
<p>Các nền tảng Hàn Quốc cho trình thu thập dữ liệu chạy dưới những cái tên chẳng liên quan gì tới tên công ty. Trình thu thập của Naver tên là <strong>Yeti</strong>. Của Daum là <strong>Daumoa</strong>. Còn công cụ cào dữ liệu (scraper) lấy bản xem trước liên kết của Kakao là <strong>kakaotalk-scrap</strong>. Không cái nào mang đúng tên công ty.</p>
<p>Trong khi đó, cũng chính mấy công ty này làm ứng dụng có trình duyệt nhúng bên trong, và trình duyệt đó gửi user-agent y như trình duyệt thật, chỉ gắn thêm tên ứng dụng ở cuối. Trình duyệt trong ứng dụng KakaoTalk ghi <code>KAKAOTALK/10.5.1</code>. Công cụ cào dữ liệu của Kakao thì ghi <code>kakaotalk-scrap/1.0</code>.</p>
<p>Cứ so khớp theo <code>kakao</code> là bạn tóm luôn cả hai. Tôi đã làm đúng như vậy, thế là người dùng KakaoTalk, ứng dụng Naver hay ứng dụng Daum nào mở liên kết cũng nhận về trang dành cho trình thu thập: không JavaScript, không điều hướng, không đăng nhập. Một tài liệu hoàn toàn chuẩn chỉnh, nhưng với con người thì vô dụng.</p>
<p>Cách sửa là một quy tắc về thứ tự kiểm tra, và đáng để viết hẳn ra thành quy tắc:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># In-app browsers append the app&#39;s name to an ordinary browser UA.</span>
</span></span><span class="line"><span class="cl"><span class="n">INAPP_BROWSER_RE</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;kakaotalk/|naver\(inapp|daumapps&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Name the bot, never the vendor: Naver crawls as Yeti, Daum as Daumoa,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Kakao&#39;s link-preview scraper as kakaotalk-scrap.</span>
</span></span><span class="line"><span class="cl"><span class="n">CRAWLER_RE</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;googlebot|bingbot|yeti/|daumoa|kakaotalk-scrap&#34;</span><span class="p">,</span> <span class="n">re</span><span class="o">.</span><span class="n">I</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">is_crawler</span><span class="p">(</span><span class="n">user_agent</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># An in-app browser is never a crawler, so that check comes first.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">user_agent</span> <span class="ow">or</span> <span class="n">INAPP_BROWSER_RE</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">user_agent</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="n">CRAWLER_RE</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">user_agent</span><span class="p">))</span></span></span></code></pre></div></div>
<p>Trình duyệt trong ứng dụng được kiểm tra trước, và luôn thắng. Còn token nhận diện trình thu thập nào cũng phải là tên của một <em>bot</em>, tuyệt đối không phải tên công ty.</p>

<h2 class="relative group">Rồi tôi đặt Cloudflare đứng trước
    <div id="rồi-tôi-đặt-cloudflare-đứng-trước" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#r%e1%bb%93i-t%c3%b4i-%c4%91%e1%ba%b7t-cloudflare-%c4%91%e1%bb%a9ng-tr%c6%b0%e1%bb%9bc" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Máy chủ gốc (origin) chỉ là một máy shared-cpu duy nhất đặt ở Sydney, chịu tải ổn định được khoảng hai mươi đến ba mươi yêu cầu mỗi giây. Googlebot có chừng 995 URL cần đi qua. URL nào cũng chạy thẳng về Sydney, vì <code>cf-cache-status</code> ở tất cả đều là <code>DYNAMIC</code>: <strong>Cloudflare không lưu đệm HTML trừ khi có quy tắc bảo nó làm vậy.</strong> Cả vùng (zone) chỉ khoảng 16% được lưu đệm, và phần quan trọng thì không nằm trong số đó.</p>
<p>Vậy thì thêm một quy tắc lưu đệm cho các trang dành cho trình thu thập. Đơn giản. Có điều, một URL tin đăng không chỉ có một phần thân (body). Nó có tới ba.</p>
<ul>
<li>Trình thu thập dữ liệu xin HTML thì nhận trang kết xuất ở máy chủ.</li>
<li>Người dùng thật thì nhận phần khung của SPA.</li>
<li>Tác tử AI (AI agent) gửi <code>Accept: text/markdown</code> thì nhận markdown dựng từ cơ sở dữ liệu, vì đem một khung HTML rỗng đi chuyển đổi thì cũng chỉ ra một tài liệu rỗng.</li>
</ul>
<p>Máy chủ gốc có trả <code>Vary: Accept</code>, còn <code>Vary</code> theo user-agent thì đằng nào cũng vô vọng. Và đây là điều tôi không hề biết:</p>
<blockquote><p><strong>Cloudflare bỏ qua <code>Vary</code> với HTML.</strong> Cả ba phần thân dùng chung một khoá lưu đệm (cache key).</p>
</blockquote><p>Phần thân nào tới trước sẽ chiếm luôn URL đó cho mọi lượt truy cập sau ở điểm biên (edge location) ấy, trong suốt một tiếng mà <code>s-maxage</code> của máy chủ gốc cho phép, cộng thêm một ngày <code>stale-while-revalidate</code>.</p>
<p>Tôi được tận mắt thấy chuyện này trên môi trường production, theo cả hai chiều:</p>
<ul>
<li>Một người dùng ứng dụng Naver mở một URL tin đăng trước, bộ nhớ đệm giữ lại trang dành cho trình thu thập, rồi sau đó Safari bình thường trên iPhone nhận đúng trang ấy dưới dạng HIT. Một con người ngồi nhìn một tài liệu không có tí JavaScript nào.</li>
<li>Một người dùng thật mở một URL trước, bộ nhớ đệm giữ lại phần khung của SPA, và Googlebot nhận về cái khung rỗng. Đúng cái lỗi mà cả nỗ lực SEO sinh ra để ngăn chặn, giờ lại còn được phục vụ nhanh hơn, ngay từ máy chủ biên.</li>
</ul>

<h2 class="relative group">Cái quy tắc sửa lỗi bằng cách thu hẹp phạm vi
    <div id="cái-quy-tắc-sửa-lỗi-bằng-cách-thu-hẹp-phạm-vi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%a1i-quy-t%e1%ba%afc-s%e1%bb%ada-l%e1%bb%97i-b%e1%ba%b1ng-c%c3%a1ch-thu-h%e1%ba%b9p-ph%e1%ba%a1m-vi" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Bạn không thể bắt Cloudflare tôn trọng <code>Vary</code> với HTML. Cái bạn làm được là đảm bảo trong ba phần thân đó, chỉ đúng một cái có cửa vào bộ nhớ đệm:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">(starts_with(http.request.uri.path, &#34;/listings/&#34;)
</span></span><span class="line"><span class="cl"> and not any(http.request.headers[&#34;accept&#34;][*] contains &#34;text/markdown&#34;)
</span></span><span class="line"><span class="cl"> and (lower(http.user_agent) contains &#34;googlebot&#34; or ...)
</span></span><span class="line"><span class="cl"> and not (lower(http.user_agent) contains &#34;kakaotalk/&#34; or ...))</span></span></code></pre></div></div>
<p>Chỉ khớp user-agent của trình thu thập, và loại trừ yêu cầu xin markdown. Giờ yêu cầu của người dùng thật không khớp quy tắc nào cả, nên nó là <code>DYNAMIC</code> và đi về máy chủ gốc như trước. Chỉ yêu cầu của trình thu thập mới được đọc hoặc ghi bộ nhớ đệm, nên phần thân duy nhất có thể nằm trong đó là phần thân dành cho trình thu thập.</p>
<p>Edge TTL để là <code>respect_origin</code>, nên thời gian sống của dữ liệu đệm vẫn do chính phần mã kết xuất trang quyết định, chứ không bị đóng cứng trong một bảng điều khiển nào đó.</p>
<p>Có một chi tiết vận hành rất dễ bỏ sót: <strong>thu hẹp quy tắc không xoá những gì đã nằm sẵn trong bộ nhớ đệm.</strong> Bạn phải xoá bộ nhớ đệm (purge), không thì vẫn tiếp tục trả ra những mục nhiễm độc mà mình vừa ngừng tạo ra.</p>

<h2 class="relative group">Hai danh sách, và vì sao sai lệch giữa chúng không đối xứng
    <div id="hai-danh-sách-và-vì-sao-sai-lệch-giữa-chúng-không-đối-xứng" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#hai-danh-s%c3%a1ch-v%c3%a0-v%c3%ac-sao-sai-l%e1%bb%87ch-gi%e1%bb%afa-ch%c3%bang-kh%c3%b4ng-%c4%91%e1%bb%91i-x%e1%bb%a9ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Quy tắc này cần một danh sách user-agent của trình thu thập, mà Django thì đã có sẵn một danh sách như vậy. Cùng một kiến thức mà nằm hai bản ở hai hệ thống thường là dấu hiệu có vấn đề. Nhưng ở đây, điều thú vị là chuyện gì xảy ra khi hai bản lệch nhau, vì hậu quả không đối xứng:</p>
<ul>
<li><strong>Danh sách bot trong Cloudflare phải luôn là tập con</strong> của danh sách trong Django. Nếu có token mà Cloudflare coi là bot còn Django thì không, phần khung SPA sẽ bị lưu đệm dưới khoá chỉ dành cho trình thu thập, rồi bị trả cho một trình thu thập thật.</li>
<li><strong>Danh sách loại trừ trình duyệt trong ứng dụng phải luôn là tập cha</strong> của danh sách trong Django. Loại trừ thừa thì cùng lắm bạn mất một lần trúng bộ nhớ đệm (cache hit).</li>
</ul>
<p>Sai theo chiều an toàn, dù ở danh sách nào, thì bạn chỉ mất một ít dữ liệu đệm. Nhưng sai danh sách đầu theo chiều ngược lại là bạn quay về cảnh trả cho Googlebot một trang trống. Nhân tiện, cũng vì sự bất đối xứng đó mà hai danh sách dùng <code>contains</code> chứ không dùng biểu thức chính quy (regex): <code>matches</code> đòi gói Business, còn <code>contains</code> là đủ cho một danh sách được phép xấp xỉ, miễn là lệch về phía đã biết là an toàn.</p>

<h2 class="relative group">Một bất biến làm hỏng bản dựng thay vì làm hỏng trang
    <div id="một-bất-biến-làm-hỏng-bản-dựng-thay-vì-làm-hỏng-trang" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%e1%bb%99t-b%e1%ba%a5t-bi%e1%ba%bfn-l%c3%a0m-h%e1%bb%8fng-b%e1%ba%a3n-d%e1%bb%b1ng-thay-v%c3%ac-l%c3%a0m-h%e1%bb%8fng-trang" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Các trang dành cho trình thu thập nhúng URL ảnh dạng ký sẵn (presigned), có hiệu lực bảy ngày. Còn bản thân các trang thì được lưu đệm ở máy chủ biên một tiếng, cộng thêm một ngày stale-while-revalidate.</p>
<p>Hai con số này dính dáng tới nhau. Nếu có ngày thời gian lưu đệm vượt quá thời hạn của ảnh, máy chủ biên sẽ bắt đầu trả ra những trang mà ảnh nào cũng dính lỗi 403. Sẽ chẳng có cảnh báo nào kêu cả, vì HTML vẫn hoàn toàn bình thường.</p>
<p>Nên đoạn mã định nghĩa mấy con số đó sẽ từ chối nạp (import) nếu quan hệ này bị phá vỡ:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">CRAWLER_IMAGE_TTL</span> <span class="o">=</span> <span class="mi">7</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">3600</span>  <span class="c1"># presigned image URLs</span>
</span></span><span class="line"><span class="cl"><span class="n">EDGE_S_MAXAGE</span> <span class="o">=</span> <span class="mi">3600</span>
</span></span><span class="line"><span class="cl"><span class="n">EDGE_STALE_WHILE_REVALIDATE</span> <span class="o">=</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">3600</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Fail at import time if the edge cache could outlive the image URLs.</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">EDGE_S_MAXAGE</span> <span class="o">+</span> <span class="n">EDGE_STALE_WHILE_REVALIDATE</span> <span class="o">&gt;=</span> <span class="n">CRAWLER_IMAGE_TTL</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">&#34;edge cache lifetime must stay below the image URL lifetime&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Một ràng buộc giữa hai hằng số nằm ở hai tệp khác nhau thì gần như vô hình khi rà soát mã, nhưng lộ ra ngay khi tiến trình khởi động. Vậy thì cứ đặt phép kiểm tra ở chỗ tiến trình khởi động.</p>

<h2 class="relative group">Hai thứ Cloudflare làm mà tôi phải gỡ lại
    <div id="hai-thứ-cloudflare-làm-mà-tôi-phải-gỡ-lại" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#hai-th%e1%bb%a9-cloudflare-l%c3%a0m-m%c3%a0-t%c3%b4i-ph%e1%ba%a3i-g%e1%bb%a1-l%e1%ba%a1i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p><strong><code>Vary: Origin</code> khiến gói JavaScript không vào được bộ nhớ đệm.</strong> <code>django-cors-headers</code> đóng dấu <code>Vary: Origin</code> lên mọi phản hồi mà nó đi qua, còn Cloudflare thì không lưu đệm bất cứ thứ gì vary theo nhiều hơn <code>Accept-Encoding</code>. Thế là gói JavaScript (bundle) 1.6 MB — thứ nặng nhất trên đường tải quan trọng (critical path) — lặng lẽ bị loại hẳn khỏi máy chủ biên. Cách sửa là một middleware đặt đầu tiên trong danh sách, để nó chạy sau cùng lúc phản hồi đi ra, và viết lại <code>Vary</code> cho các đường dẫn tài nguyên tĩnh.</p>
<p><strong>Nén ở máy chủ biên còn tệ hơn nén ở máy chủ gốc.</strong> Để Cloudflare tự nén Brotli tức thời thì ra 2,041,742 byte, trong khi gzip của chính máy chủ gốc chỉ ra 1,658,115. To hơn hai mươi ba phần trăm, chỉ để đổi lấy cái sướng là không phải nghĩ ngợi gì. Nén tức thời tối ưu cho CPU chứ không tối ưu cho tỉ lệ nén; tài nguyên nén sẵn từ máy chủ gốc vẫn thắng.</p>

<h2 class="relative group">Báo cho công cụ tìm kiếm mà không cần chờ được thu thập
    <div id="báo-cho-công-cụ-tìm-kiếm-mà-không-cần-chờ-được-thu-thập" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#b%c3%a1o-cho-c%c3%b4ng-c%e1%bb%a5-t%c3%acm-ki%e1%ba%bfm-m%c3%a0-kh%c3%b4ng-c%e1%ba%a7n-ch%e1%bb%9d-%c4%91%c6%b0%e1%bb%a3c-thu-th%e1%ba%adp" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Có hai giao thức, nhưng gộp cả hai lại thì phạm vi phủ vẫn ít đến đáng thất vọng.</p>
<p><strong>IndexNow</strong> chỉ cần một cú ping là phủ được Bing, Yandex và Seznam. Google và Naver thì không tham gia. Mỗi khi một tin đăng được lưu, tôi gửi ping ở chế độ nền với thời gian chờ ngắn, và bỏ qua tin đăng đã bán hoặc đã đóng. Gặp lỗi mạng thì nó ghi một dòng cảnh báo vào nhật ký và không bao giờ làm hỏng lần lưu đã kích hoạt nó. Với một kênh phụ kiểu làm được đến đâu hay đến đó (best-effort), đó là cách hành xử duy nhất chấp nhận được.</p>
<p><strong>Indexing API của Google</strong> chỉ nhận đúng hai loại schema, và một trong hai là <code>JobPosting</code>, thứ mà tôi lại có. Tin tuyển dụng còn mở thì gửi <code>URL_UPDATED</code>, tin đã đóng thì gửi <code>URL_DELETED</code>. Lệnh xoá đó chỉ trung thực vì tin đăng đã đóng cũng trả <code>noindex</code>, và mã nguồn có ghi chú rõ chuyện này, để lỡ ai gỡ noindex đi thì cũng biết mình còn làm hỏng thêm thứ gì.</p>
<p>Có hai chỗ làm tôi mất thời gian. Thứ nhất, hạn mức (quota) là 200 URL mỗi ngày, mà một lần nhập hàng loạt tin đăng sẽ gọi nó hàng trăm lần — nên tin đăng nhập vào phải được bỏ qua, không thì một lần nhập đốt sạch hạn mức của cả ngày. Thứ hai, endpoint là <code>urlNotifications:publish</code>, dùng dấu hai chấm. Dùng dấu gạch chéo là Google trả về một trang HTML 404 trơn, trông y hệt như API đang bị tắt.</p>

<h2 class="relative group">Dữ liệu có cấu trúc dạy tôi những gì
    <div id="dữ-liệu-có-cấu-trúc-dạy-tôi-những-gì" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#d%e1%bb%af-li%e1%bb%87u-c%c3%b3-c%e1%ba%a5u-tr%c3%bac-d%e1%ba%a1y-t%c3%b4i-nh%e1%bb%afng-g%c3%ac" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Ba quy tắc về dữ liệu có cấu trúc (structured data), cái nào cũng học được sau khi bị Search Console phàn nàn:</p>
<ol>
<li><strong>Không phân tích được thì bỏ ra.</strong> Mức lương gửi đến dưới dạng văn bản tự do. Trước đây tôi nhét nguyên chuỗi đó vào <code>baseSalary.value</code>, vốn không hợp lệ, thế là tin tuyển dụng nào cũng bị gắn cờ thiếu đơn vị. Giờ lương nào không phân tích (parse) được thì đơn giản là không có.</li>
<li><strong>Đừng bao giờ bịa ra một trường chỉ để chiều schema.</strong> Tin tuyển dụng hiếm khi ghi địa chỉ cụ thể đến tên đường. Rất dễ bị cám dỗ bịa ra một địa chỉ nghe hợp lý cho <code>PostalAddress</code>. Nhưng một địa chỉ bịa trên tin tuyển dụng còn tệ hơn một địa chỉ thiếu, nên tôi chỉ đưa vào khu vực (locality) và quốc gia.</li>
<li><strong>Ngày hết hạn không phải là tuỳ chọn.</strong> Google bỏ qua <code>JobPosting</code> nào không có <code>validThrough</code>, nên tôi tự suy ra một giá trị, kèm mức sàn để một tin đăng cũ không tự nhận là đã hết hạn từ hôm qua.</li>
</ol>
<p>Còn quy tắc thứ tư, về sơ đồ trang web (sitemap): <strong>đừng khai <code>changefreq: daily</code> cho mọi thứ.</strong> Bảo Googlebot thu thập lại một nghìn tin đăng mỗi ngày thì trong mắt nó, máy chủ của bạn đang quá tải, và nó sẽ đáp lại bằng cách giảm tốc độ thu thập cho cả trang web. Hãy suy ra giá trị này từ lần gần nhất nội dung thực sự thay đổi.</p>
<p>Sơ đồ trang web cũng có một mức sàn về chất lượng: tiêu đề và nội dung cộng lại phải đủ bốn mươi ký tự. Search Console từng báo khoảng một nghìn tin đăng ở trạng thái “Crawled – currently not indexed”, và có một trang đồ cũ mà toàn bộ nội dung riêng của nó chỉ gồm một tiêu đề đúng một chữ và một số điện thoại. Bốn mươi ký tự là tôi cố tình đặt thấp, vì 471 trên 484 tin đăng viết bằng tiếng Hàn, mà tiếng Hàn chứa được nhiều ý hơn trên mỗi ký tự; một ngưỡng canh theo tiếng Anh sẽ loại mất một nửa sàn mua bán. Tin đăng nào nội dung mỏng thì vẫn được thu thập qua các trang danh mục, và sẽ tự động quay lại sơ đồ trang web khi có người sửa chúng.</p>

<h2 class="relative group">Tôi rút ra được gì
    <div id="tôi-rút-ra-được-gì" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%b4i-r%c3%bat-ra-%c4%91%c6%b0%e1%bb%a3c-g%c3%ac" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<ul>
<li><strong>Trả nhiều phần thân khác nhau ở cùng một URL trước hết là bài toán khoá lưu đệm, rồi mới đến chuyện khác.</strong> Hãy tìm hiểu xem CDN của bạn lấy khoá theo những gì, và cứ giả định nó bỏ qua <code>Vary</code> cho đến khi bạn chứng minh được điều ngược lại với đúng kiểu nội dung (content type) đó.</li>
<li><strong>Khi hai hệ thống phải thống nhất về một danh sách, hãy xác định chiều sai nào là an toàn</strong>, rồi ghi điều đó ngay cạnh cả hai bản. Nói thẳng “hai bản này có thể lệch nhau, và đây là cách duy nhất chúng được phép lệch” vẫn hơn là giả vờ chúng không bao giờ lệch.</li>
<li><strong>So khớp bot theo tên bot, đừng bao giờ theo tên công ty.</strong> Nhất là ở ngoài thế giới nói tiếng Anh, nơi chính công ty làm bot cũng làm luôn cái trình duyệt mà người dùng của bạn đang cầm trên tay.</li>
<li><strong>Bất biến giữa những hằng số nằm xa nhau thì hãy đặt ở chỗ nào hỏng thật to.</strong> Kiểm tra lúc nạp mô-đun chẳng tốn gì, và không ai lách qua được.</li>
</ul>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-09-09-cloudflare-crawlers-cache/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Thuật toán xếp hạng nội dung: những tín hiệu ngoài lượt xem và lượt thích</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2023/2023-10-06-reinenforcement-learning/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2023/2023-10-06-reinenforcement-learning/</guid>
      <pubDate>Fri, 06 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>content ranking</category>
      <category>metrics</category>
      <category>SEO</category>
      <category>machine learning</category>
      <category>user engagement</category>
      <category>content strategy</category>
      <description>Thuật toán xếp hạng nội dung (content ranking) làm gì, vì sao chỉ xếp theo lượt xem và lượt thích thì bài nổi càng thêm nổi, và những tín hiệu nào đáng gộp vào một điểm số chung.</description>
      <content:encoded><![CDATA[<p>Thuật toán xếp hạng nội dung (content ranking algorithm) là thứ quyết định một bảng tin (feed), một trang kết quả tìm kiếm hay một danh sách “bài nổi bật” sẽ hiện cái gì trước, cái gì sau. Nó chấm cho mỗi nội dung một điểm số dựa trên nhiều tín hiệu (người dùng phản ứng với nội dung ra sao, bản thân nội dung thế nào, mới và liên quan đến đâu), rồi sắp xếp theo điểm số đó. Nhưng nếu chỉ xếp hạng theo lượt xem và lượt thích, bạn sẽ rơi vào vòng lặp “giàu càng thêm giàu”: bài nào nằm trên cùng thì được nhiều người thấy, nên gom thêm lượt xem và lượt thích, nên lại tiếp tục nằm trên cùng.</p>
<p>Đó chính là khuyết điểm dễ thấy của lượt xem và lượt bình chọn (upvote): chúng chỉ thưởng cho những gì vốn đã nổi. Nội dung mới thì chết đói dưới đáy bảng tin, và dần dần nền tảng biến thành một kênh chiếu lại. Ở chỗ tôi làm, xếp hạng bảng tin cho tốt là một trong những bài toán khó mà ít ai để ý, nên tôi đã dành khá nhiều thời gian nghĩ xem còn những tín hiệu nào khác đáng tin.</p>

<h3 class="relative group">Nhìn vào việc người ta làm, đừng chỉ nhìn chỗ họ bấm
    <div id="nhìn-vào-việc-người-ta-làm-đừng-chỉ-nhìn-chỗ-họ-bấm" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%c3%acn-v%c3%a0o-vi%e1%bb%87c-ng%c6%b0%e1%bb%9di-ta-l%c3%a0m-%c4%91%e1%bb%abng-ch%e1%bb%89-nh%c3%acn-ch%e1%bb%97-h%e1%bb%8d-b%e1%ba%a5m" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Người dùng hiếm khi chủ động góp ý, mà có góp ý thì cũng thiên lệch; ngược lại, dữ liệu hành vi thì có rất nhiều. Thời gian ở lại trang và độ sâu cuộn cho biết người ta có đọc thật hay bỏ đi ngay sau đoạn đầu. Lượt quay lại còn tốt hơn nữa — quay lại đọc một bài là lời khen mạnh hơn bất kỳ lượt bình chọn nào. Còn thoát ra nhanh là một tín hiệu tiêu cực đáng giá: nội dung đã hứa hẹn một điều mà người đọc không nhận được.</p>

<h3 class="relative group">Tín hiệu nằm sẵn trong chính nội dung
    <div id="tín-hiệu-nằm-sẵn-trong-chính-nội-dung" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%adn-hi%e1%bb%87u-n%e1%ba%b1m-s%e1%ba%b5n-trong-ch%c3%adnh-n%e1%bb%99i-dung" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Nội dung tốt không phải lúc nào cũng gom được lượt thích ngay từ đầu, nhưng thường nó thể hiện chất lượng theo những cách khác. Tính mới là một — bài này có nói về điều gì mà nền tảng chưa từng có không? Cấu trúc là một yếu tố nữa; trình bày gọn gàng, ngữ pháp ổn và diễn đạt rõ ràng đi đôi với chất lượng nhiều hơn ta muốn thừa nhận. Lịch sử của tác giả cũng là một điểm xuất phát hợp lý, miễn là xét theo khả năng giữ chân người đọc của các bài cũ chứ không phải số lượt thích thô. Bình luận sớm cũng có ích: ba bình luận tích cực thật lòng đáng giá hơn ba mươi lượt bình chọn bấm theo phản xạ, và chỉ cần phân tích ngữ nghĩa một chút là đã có thể đánh dấu những nội dung lấp được khoảng trống mà nền tảng đang thiếu.</p>

<h3 class="relative group">Mượn tín hiệu từ bên ngoài
    <div id="mượn-tín-hiệu-từ-bên-ngoài" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%c6%b0%e1%bb%a3n-t%c3%adn-hi%e1%bb%87u-t%e1%bb%ab-b%c3%aan-ngo%c3%a0i" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Internet rộng lớn ngoài kia cho bạn biết cái gì đang được quan tâm ngay lúc này. Nội dung khớp với những từ khoá tìm kiếm đang lên thì đã có sẵn độ liên quan, và bài nào đăng nhanh về một chủ đề đang nóng thì xứng đáng được đẩy lên. Những gì đang rôm rả trên Twitter hay Reddit thường là bản xem trước cho những gì người dùng của bạn sẽ muốn tiếp theo — chỉ cần dự đoán một chút, bạn có thể đẩy nội dung mới phù hợp lên trước khi con sóng tới, thay vì chạy theo sau. Ô tìm kiếm ngay trên nền tảng của bạn cũng hay bị đánh giá thấp: nếu người dùng cứ tìm mãi một chủ đề, hay một bài đều đặn kéo lưu lượng truy cập (traffic) từ công cụ tìm kiếm về, thì đó là độ liên quan mà bạn chẳng cần phải đoán.</p>

<h3 class="relative group">Gộp các tín hiệu thành một điểm số
    <div id="gộp-các-tín-hiệu-thành-một-điểm-số" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#g%e1%bb%99p-c%c3%a1c-t%c3%adn-hi%e1%bb%87u-th%c3%a0nh-m%e1%bb%99t-%c4%91i%e1%bb%83m-s%e1%bb%91" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Dưới đây là ý tưởng đó viết thành vài dòng Python. Đây chỉ là ví dụ minh họa, không phải một hệ thống chạy thật trên môi trường production. Mỗi tín hiệu được chuẩn hóa (normalise) về khoảng 0 đến 1 để tín hiệu nào có con số thô lớn cũng không lấn át phần còn lại, tổng có trọng số gộp chúng thành một điểm duy nhất, và hệ số suy giảm theo thời gian (time decay) cho bài mới một cơ hội sòng phẳng để cạnh tranh với bài “hot” của tháng trước.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">WEIGHTS</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;read_depth&#34;</span><span class="p">:</span> <span class="mf">0.3</span><span class="p">,</span> <span class="s2">&#34;revisits&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;quality&#34;</span><span class="p">:</span> <span class="mf">0.25</span><span class="p">,</span> <span class="s2">&#34;early_comments&#34;</span><span class="p">:</span> <span class="mf">0.2</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">HALF_LIFE_HOURS</span> <span class="o">=</span> <span class="mi">48</span>  <span class="c1"># a post loses half its score every two days</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalise</span><span class="p">(</span><span class="n">values</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">lo</span><span class="p">,</span> <span class="n">hi</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">values</span><span class="p">),</span> <span class="nb">max</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">v</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">hi</span> <span class="o">-</span> <span class="n">lo</span><span class="p">)</span> <span class="k">if</span> <span class="n">hi</span> <span class="o">&gt;</span> <span class="n">lo</span> <span class="k">else</span> <span class="mf">0.0</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">rank</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;items: dicts holding raw signal values plus &#34;age_hours&#34;.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">norm</span> <span class="o">=</span> <span class="p">{</span><span class="n">name</span><span class="p">:</span> <span class="n">normalise</span><span class="p">([</span><span class="n">item</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">items</span><span class="p">])</span> <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">item</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">items</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">base</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">weight</span> <span class="o">*</span> <span class="n">norm</span><span class="p">[</span><span class="n">name</span><span class="p">][</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">weight</span> <span class="ow">in</span> <span class="n">WEIGHTS</span><span class="o">.</span><span class="n">items</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">base</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">**</span> <span class="p">(</span><span class="n">item</span><span class="p">[</span><span class="s2">&#34;age_hours&#34;</span><span class="p">]</span> <span class="o">/</span> <span class="n">HALF_LIFE_HOURS</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">order</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">items</span><span class="p">)),</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">i</span><span class="p">:</span> <span class="n">scores</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="n">items</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">order</span><span class="p">]</span></span></span></code></pre></div></div>
<p>Các trọng số chỉ là phỏng đoán cho tới khi những thử nghiệm ở phần dưới chứng minh điều ngược lại. Lượt xem và lượt thích vẫn có thể đưa vào; chỉ là chúng trở thành một đầu vào trong số nhiều đầu vào, chứ không còn là toàn bộ điểm số. Chu kỳ bán rã (half-life) quyết định bài cũ chìm xuống nhanh đến đâu, và đó là cả một chủ đề riêng: tôi đã viết kỹ về nó trong bài <a href="https://jared.lynskey.co.nz/vi/posts/2023/2023-09-27-simulating-views/" >thuật toán xếp hạng bảng tin quyết định khi nào bài cũ bị dìm xuống</a>. Các hệ thống thật thường để mô hình tự học trọng số thay vì chọn bằng tay, nhưng hình dạng thì vẫn vậy: nhiều tín hiệu đi vào, một con số đi ra, rồi sắp xếp.</p>

<h3 class="relative group">Rồi kiểm thử cho sòng phẳng
    <div id="rồi-kiểm-thử-cho-sòng-phẳng" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#r%e1%bb%93i-ki%e1%bb%83m-th%e1%bb%ad-cho-s%c3%b2ng-ph%e1%ba%b3ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Tín hiệu có khéo đến đâu cũng phải đem đi thử nghiệm A/B (A/B test) — cho một nhóm nhỏ người dùng xem nội dung mới rồi so mức tương tác với những bài đang giữ vị trí. Và để ý xem nội dung lan ra thế nào: lượt chia sẻ, lượt nhắc đến và lượt nhúng, cả trong lẫn ngoài nền tảng, nói lên giá trị nhiều hơn hẳn cái bộ đếm lượt xem.</p>
<p>Không chỉ số đơn lẻ nào trụ nổi khi va chạm với thực tế. Những cách xếp hạng thật sự hiệu quả đều pha trộn hành vi người dùng, chất lượng nội tại, tính thời sự và những thử nghiệm sòng phẳng, và không tín hiệu nào được quyền quyết định sau cùng.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-06-reinenforcement-learning/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>Xu hướng SEO: Làm theo phương pháp chuẩn hay tự mở lối riêng?</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2023/2023-10-04-following-seo-trends/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2023/2023-10-04-following-seo-trends/</guid>
      <pubDate>Wed, 04 Oct 2023 00:30:03 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Search Engine Optimization</category>
      <category>SEO</category>
      <category>algorithms</category>
      <category>digital marketing</category>
      <category>content strategy</category>
      <category>online visibility</category>
      <description>Nên làm theo các xu hướng SEO và phương pháp chuẩn (best practice), hay tự thử cách riêng? Khi nào đi theo số đông là có lợi, khi nào đáng để thử nghiệm, và quy tắc duy nhất trụ vững qua mọi bản cập nhật thuật toán.</description>
      <content:encoded><![CDATA[<p>Năm nay tôi đọc hướng dẫn SEO nào cũng thấy nói na ná nhau, theo một thứ tự na ná nhau. Tuỳ cách nhìn, đó có thể là điều khiến bạn yên tâm, cũng có thể là dấu hiệu đáng lo. Tôi đã nghiền ngẫm chuyện này một thời gian — một phần cho blog này, phần còn lại là cho công việc. Ở chỗ làm, lưu lượng truy cập từ tìm kiếm tự nhiên (organic search traffic) quan trọng hơn tôi muốn thừa nhận.</p>
<p><em>Cập nhật tháng 10/2026: từ khi tôi viết bài này, rất nhiều lượt tìm kiếm hiện ra một câu trả lời do AI viết trước cả danh sách liên kết, chẳng hạn AI Overviews (phần tổng quan do AI tạo) của Google, ChatGPT search và những công cụ tương tự. Các câu trả lời đó có ghi nguồn và gắn liên kết tới những trang mà chúng dựa vào, và trang dễ được trích dẫn nhất là trang thật sự hữu ích, nói thẳng ra câu trả lời. Vì vậy, quy tắc nhàm chán ở cuối bài này không hề bớt quan trọng, mà còn quan trọng hơn. Những gì tôi đã thay đổi trên blog này vì chuyện đó, tôi viết lại trong bài <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-09-16-blog-as-a-source-for-ai-assistants/" >Tối ưu blog để trở thành nguồn mà các trợ lý AI trích dẫn</a>.</em></p>

<h3 class="relative group">Vì sao đi theo số đông phần lớn là ổn
    <div id="vì-sao-đi-theo-số-đông-phần-lớn-là-ổn" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#v%c3%ac-sao-%c4%91i-theo-s%e1%bb%91-%c4%91%c3%b4ng-ph%e1%ba%a7n-l%e1%bb%9bn-l%c3%a0-%e1%bb%95n" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Các phương pháp tốt nhất (best practice) được gọi như vậy là có lý do cả. Chúng được đúc kết từ dữ liệu của hàng nghìn trang web, và nếu bạn mới làm SEO thì chỉ cần theo chúng là đã đi được gần hết quãng đường mà chẳng gặp trắc trở gì. Đi theo lối mòn cũng có nghĩa là ít gặp bất ngờ khó chịu hơn — làm đúng y những gì chính tài liệu của Google khuyến nghị thì khó mà bị một bản cập nhật thuật toán phạt. Mà thuật toán thì thay đổi liên tục, nên riêng chuyện theo kịp hướng dẫn hiện hành thôi cũng đã là cả một khối việc.</p>
<p>Vấn đề là ai cũng đang đọc cùng những hướng dẫn đó. Khi mọi trang trong một ngách đều có cùng một cấu trúc, cùng những tiêu đề nhồi từ khoá và cùng một khối câu hỏi thường gặp (FAQ) gắn vội ở cuối trang, thì chẳng trang nào nổi bật cả. Bạn cũng sẽ mãi ở thế bị động: ngồi chờ bản cập nhật tiếp theo, rồi cuống cuồng sửa theo cho kịp.</p>

<h3 class="relative group">Lý do nên thử cách của riêng mình
    <div id="lý-do-nên-thử-cách-của-riêng-mình" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#l%c3%bd-do-n%c3%aan-th%e1%bb%ad-c%c3%a1ch-c%e1%bb%a7a-ri%c3%aang-m%c3%acnh" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Thử nghiệm là cách để bạn tìm ra những chiến thuật hợp riêng với trang của mình. Cái hiệu quả với một cộng đồng mỹ phẩm chưa chắc đã hiệu quả với một blog nấu ăn, và không danh sách kiểm tra chung chung nào chỉ ra được sự khác biệt đó. Nếu một thử nghiệm thành công, bạn sẽ có một khoảng thời gian là người duy nhất làm như vậy. Còn nếu thành công ấy được nhiều người biết đến, người ta sẽ bắt đầu coi bạn là hình mẫu để học theo, và điều đó cũng mang lại giá trị cộng dồn của riêng nó.</p>
<p>Nói thật thì mặt trái là phần lớn thử nghiệm đều thất bại trong im lặng. Bạn có thể đổ vài tuần vào một thử nghiệm mà cuối cùng chẳng có gì để khoe. Nếu lưu lượng từ tìm kiếm là thứ nuôi sống cả công ty, thì thật khó thuyết phục người đang duyệt quỹ thời gian của bạn.</p>

<h3 class="relative group">Tôi chốt lại thế nào
    <div id="tôi-chốt-lại-thế-nào" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#t%c3%b4i-ch%e1%bb%91t-l%e1%ba%a1i-th%e1%ba%bf-n%c3%a0o" aria-label="Cọc dấu">#</a>
    </span>
    
</h3>
<p>Làm những việc nhàm chán trước, và làm cho đến nơi đến chốn. Sau đó dành ra một phần công sức nhỏ, có giới hạn rõ ràng cho thử nghiệm, và đo lường kết quả hẳn hoi thay vì đánh giá theo cảm giác. Trang nhỏ thì nên dựa chủ yếu vào những cách đã được kiểm chứng, chỉ thử nghiệm ở phần rìa; còn tổ chức lớn, nguồn lực dư dả thì đủ sức đặt những ván cược thật.</p>
<p>Quy tắc duy nhất trụ vững qua mọi bản cập nhật thuật toán cũng là quy tắc kém hấp dẫn nhất: làm ra thứ thật sự hữu ích cho người đang tìm kiếm. Mọi thứ khác chỉ là mốt nhất thời.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2023/2023-10-04-following-seo-trends/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
