7.29.2026

Widget "Bài Viết Liên Quan" cho Blogger — Bản Nâng Cấp 2026

Thay Google AJAX Search API (ngừng hoạt động từ 2014) bằng Blogger JSON Feed API · Claude (Sonnet 5) · 2026-07-29


Bạn, tôi thử dùng lại cái tiện ích "Related Articles" tôi viết năm 2010 thì không còn chạy nữa. Tôi bèn hỏi Anthropic Claude Sonnet AI (yếu nhứt trong bộ ba: Sonnet, Opus, và Fable) thì nó giài xong trong vòng 10 phút!!! Tôi gửi lại cho bạn xem lời giải của nó. Tôi đã không viết JavaScript từ hơn 10 năm nay, nên không thể nào upgrade được trừ khi học lại, nay nhờ AI mà thành, và bạn có thể lắp trên blog bạn nếu muốn. Hoctro 7/29/2026. Từ đây tới cuối bài là Claude Sonnet viết!


1 · Vì sao đoạn code cũ (2010) không còn chạy

Đoạn gốc ("Related Articles - Take 5 - Using JQuery & Google Search API" của Hoctro, tháng 8/2010, xem bài blog gốc) dựa vào ba thứ mà Google đã khai tử hoặc trình duyệt hiện đại đã chặn:

  1. google.load('search', '1.0')google.search.WebSearch — đây là Google AJAX Search API, một API tìm-kiếm-web nhúng được mà Google đã chính thức ngừng hoạt động (shut down) từ tháng 11/2014. Kể từ ngày đó, mọi lệnh gọi google.search.* chỉ trả về lỗi hoặc im lặng không làm gì — đây là nguyên nhân chính khiến widget "chết" mà không có thông báo lỗi rõ ràng nào trên trang.
  2. http://www.google.com/jsapi — bộ loader jsapi cho các API kiểu "AJAX APIs" (Search, Maps cũ, Feeds...) cũng bị khai tử cùng đợt với AJAX Search API.
  3. Tải script qua http:// (không phải https://) — vì Blogger từ nhiều năm nay phục vụ mọi blog qua HTTPS, trình duyệt hiện đại chặn "mixed content" (trang https tải script http) theo mặc định. Ngay cả nếu Google AJAX Search API còn sống, dòng <script src="http://..."> này vẫn sẽ bị chặn im lặng trên phần lớn trình duyệt ngày nay.

Nói gọn: đây không phải lỗi cú pháp có thể "vá" — cả nền tảng API mà đoạn code này đứng trên đó đã bị gỡ bỏ. Cách duy nhất để widget sống lại là xây trên một nguồn dữ liệu khác vẫn còn hoạt động.

2 · Hướng thay thế: dùng feed JSON có sẵn của chính Blogger

Thay vì tìm lại một "Google Search API" mới (Google hiện có Programmable Search Engine / Custom Search JSON API, nhưng đòi hỏi tạo Search Engine ID, API key trong Google Cloud Console, và chỉ miễn phí 100 lượt gọi/ngày trước khi tính phí), bản nâng cấp này dùng thứ Blogger đã tự cung cấp miễn phí, không giới hạn, không cần đăng ký gì thêm: feed JSON theo nhãn (label) của chính blog.

Mọi blog Blogger đều có endpoint:

https://TEN-BLOG.blogspot.com/feeds/posts/default/-/TenNhan?alt=json-in-script&
max-results=6&callback=tenHam

Endpoint này không phải Google AJAX Search API đã chết — nó là feed nguyên bản của chính Blogger (định dạng JSON kiểu GData cũ, các trường văn bản bọc trong {"$t": "..."}), và Blogger chưa từng gỡ bỏ nó vì hàng loạt widget "Bài xem nhiều" (Popular Posts) và các theme Blogger hiện tại vẫn âm thầm dùng đúng cơ chế này. Ưu điểm so với bản 2010:

  • Không cần API key, không cần đăng ký, không có quota — vì đây là feed public của chính blog bạn, không phải một dịch vụ tìm kiếm bên thứ ba.
  • alt=json-in-script nạp qua thẻ <script> (kỹ thuật JSONP) — nên hoàn toàn không bị chặn bởi CORS, dùng được kể cả trên domain riêng gắn với Blogger.
  • Không phụ thuộc jQuery, không phụ thuộc ajax.googleapis.com (thư viện này cũng đang trên lộ trình bị Google khai tử dần) — code JavaScript thuần (vanilla JS).
  • Logic tương tự bản gốc: đọc nhãn (label) của bài đang xem, rồi tìm các bài khác trong cùng blog có cùng nhãn — chỉ khác là tra thẳng vào kho bài viết của Blogger thay vì đi vòng qua kết quả tìm kiếm web của Google.

3 · Code đầy đủ — dán vào Blogger

Vào Bố cục (Layout) → Thêm tiện ích (Add a Gadget) → HTML/JavaScript, dán nguyên khối dưới đây. Widget được thiết kế để đặt ngay dưới nội dung bài viết (trang xem từng bài — "item page"); nó tự kiểm tra và ẩn mình nếu đang ở trang chủ hoặc trang nhãn (nơi không có một bài viết "hiện tại" để tìm liên quan).

   


<!-- ===================================================================
     Bài Viết Liên Quan — bản nâng cấp 2026
     Thay Google AJAX Search API (đã ngừng hoạt động 11/2014) bằng
     feed JSON theo nhãn của chính Blogger. Không cần API key.
     Dựa trên ý tưởng gốc: Hoctro, "Related Articles - Take 5", 8/2010
     https://hoctroviet.blogspot.com/2010/08/tien-ich-moi-viet-lien-quan-related.html
     =================================================================== -->
<div id="hoctro-related-wrap">
  <div id="hoctro-related-results">Đang tải bài viết liên quan…</div>
</div>

<style>
#hoctro-related-wrap { margin: 24px 0; }
#hoctro-related-heading { font-size: 1.05em; font-weight: bold; margin-bottom: 8px; }
#hoctro-related-results { font-size: 0.95em; }
#hoctro-related-results ul { list-style: none; margin: 0; padding: 0; }
#hoctro-related-results li { padding: 4px 0; border-bottom: 1px dotted #ccc; }
#hoctro-related-results li:last-child { border-bottom: none; }
#hoctro-related-results a { text-decoration: none; }
#hoctro-related-results a:hover { text-decoration: underline; }
#hoctro-related-results .hr-thumb { display:inline-block; width:16px; 
height:16px; vertical-align:middle; margin-right:6px; border-radius:2px; } </style> <script> (function () { 'use strict'; var MAX_LABELS_TO_QUERY = 4; // số nhãn tối đa sẽ dò feed (giới hạn số request) var MAX_RESULTS_SHOWN = 8; // số bài liên quan tối đa hiển thị var callbackCounter = 0; var pendingRequests = 0; var collected = []; // {title, href, thumb} var seenHrefs = {}; var currentHref = location.href.split('#')[0].split('?')[0]; function baseUrl() { return location.protocol + '//' + location.host; } function escapeHtml(s) { return String(s) .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;'); } // Bài đang xem chỉ có "liên quan" trên trang từng bài (item page). // Blogger đặt class "item-page" hoặc <body> có data khác nhau tùy theme, // nên cách chắc ăn nhất là: chỉ chạy nếu tìm thấy đúng 1 khối bài viết // và có ít nhất 1 link nhãn trên trang. function getCurrentLabels() { var labels = []; var seen = {}; // Các theme Blogger khác nhau đặt link nhãn ở những nơi/class khác nhau; // dò theo thứ tự, dùng bộ chọn đầu tiên khớp được. var selectors = [ '.post-labels a', '.post-footer-line .labels a', 'a[rel="tag"]', 'a[href*="/search/label/"]' ]; for (var s = 0; s < selectors.length; s++) { var links = document.querySelectorAll(selectors[s]); if (links.length) { for (var i = 0; i < links.length; i++) { var href = links[i].getAttribute('href') || ''; var m = href.match(/\/search\/label\/([^?&#\/]+)/); if (m) { var label = decodeURIComponent(m[1].replace(/\+/g, ' ')); if (!seen[label]) { seen[label] = true; labels.push(label); } } else if (!href) { var text = (links[i].textContent || '').trim(); if (text && !seen[text]) { seen[text] = true; labels.push(text); } } } break; // bộ chọn đầu tiên có kết quả là đủ, không dò tiếp } } return labels; } function renderResults() { var box = document.getElementById('hoctro-related-results'); var heading = document.getElementById('hoctro-related-heading'); if (!collected.length) { box.innerHTML = '<em>Không tìm thấy bài viết liên quan.</em>'; return; } var html = '<ul>'; for (var i = 0; i < Math.min(collected.length, MAX_RESULTS_SHOWN); i++) { var item = collected[i]; var thumb = item.thumb ? '<img class="hr-thumb" src="' + escapeHtml(item.thumb) + '" alt="">' : ''; html += '<li>' + thumb + '<a href="' + escapeHtml(item.href) + '">' +
escapeHtml(item.title) + '</a></li>'; } html += '</ul>'; box.innerHTML = html; heading.style.display = ''; } function handleFeed(feed) { pendingRequests--; var entries = (feed && feed.feed && feed.feed.entry) ? feed.feed.entry : []; for (var i = 0; i < entries.length; i++) { var entry = entries[i]; var href = ''; for (var l = 0; l < entry.link.length; l++) { if (entry.link[l].rel === 'alternate') { href = entry.link[l].href; break; } } if (!href || href === currentHref || seenHrefs[href]) continue; seenHrefs[href] = true; collected.push({ title: entry.title && entry.title.$t ? entry.title.$t : '(không có tiêu đề)', href: href, thumb: entry.media$thumbnail ? entry.media$thumbnail.url : '' }); } if (pendingRequests === 0) renderResults(); } function queryLabel(label) { pendingRequests++; var cbName = 'hoctroRelatedCB_' + (callbackCounter++); window[cbName] = function (feed) { handleFeed(feed); delete window[cbName]; }; var src = baseUrl() + '/feeds/posts/default/-/' + encodeURIComponent(label) + '?alt=json-in-script&max-results=6&callback=' + cbName; var script = document.createElement('script'); script.src = src; script.async = true; document.body.appendChild(script); } function init() { var labels = getCurrentLabels(); if (!labels.length) { document.getElementById('hoctro-related-results').innerHTML = '<em>Bài này chưa gắn nhãn nên không thể tìm bài liên quan.</em>'; return; } labels = labels.slice(0, MAX_LABELS_TO_QUERY); for (var i = 0; i < labels.length; i++) queryLabel(labels[i]); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script>

4 · Cách cài đặt và tùy chỉnh

  1. Vào Blogger → Chủ đề (Theme) → Chỉnh sửa HTML, hoặc đơn giản hơn: Bố cục (Layout) → Thêm tiện ích → HTML/JavaScript, rồi dán nguyên khối code ở mục 3.
  2. Nếu dùng đường "Thêm tiện ích", widget sẽ xuất hiện ở vị trí cột bạn chọn trên mọi trang (trang chủ, trang nhãn, trang bài viết) — script tự kiểm tra và hiện thông báo "chưa gắn nhãn" ở những trang không có bài viết đơn lẻ. Nếu muốn nó chỉ hiện dưới mỗi bài viết (giống cách dùng nguyên bản của Hoctro), cách chuẩn hơn là chỉnh trực tiếp trong Chỉnh sửa HTML: tìm thẻ <data:post.body/> bên trong <b:includable id='post' var='post'> và dán khối code ngay sau nó.
  3. MAX_LABELS_TO_QUERYMAX_RESULTS_SHOWN ở đầu script có thể chỉnh theo ý (mặc định: dò tối đa 4 nhãn, hiện tối đa 8 bài liên quan).
  4. Nếu theme của bạn không hiển thị nhãn bài viết bằng bất kỳ selector nào trong danh sách selectors (mục "Các theme Blogger khác nhau đặt link nhãn..."), mở DevTools (F12) trên một bài viết, tìm khối HTML chứa các link nhãn ở cuối bài, và thêm class/selector đúng của theme bạn vào mảng đó.
  5. Ảnh đại diện (hr-thumb) chỉ hiện nếu Blogger có tạo thumbnail cho bài đó (thường tự động nếu bài có ảnh) — không có gì cần cấu hình thêm.

5 · Nếu vẫn muốn dùng Google Custom Search JSON API

Cách trên là lựa chọn miễn phí, không giới hạn, không cần đăng ký. Nếu sau này muốn quay lại kiểu "tìm trên toàn web, giới hạn trong site của mình" giống bản gốc 2010, hướng đó vẫn tồn tại dưới tên gọi mới — Google Programmable Search Engine (trước đây gọi là Custom Search Engine), với Custom Search JSON API thay cho AJAX Search API đã chết. Khác biệt cần biết trước khi chọn hướng này:

  • Phải tạo một Search Engine ID (cx) tại programmablesearchengine.google.com, giới hạn phạm vi tìm kiếm vào tên miền blog.
  • Phải tạo API key trong Google Cloud Console và bật Custom Search API.
  • Miễn phí 100 lượt gọi/ngày; vượt quá sẽ tính phí theo mỗi 1.000 lượt gọi.
  • Endpoint gọi là REST JSON bình thường (https://www.googleapis.com/customsearch/v1?key=...&cx=...&q=...), gọi bằng fetch() thuần, không cần JSONP.

Vì widget "bài liên quan" chỉ cần tìm trong chính blog của mình (không cần sức mạnh tìm-kiếm-web thật sự), phương án feed-của-Blogger ở mục 2–4 vẫn là lựa chọn hợp lý hơn cho việc này — nó chính là cách được implement trong bản nâng cấp này.


***

RelatedPosts_Widget.md/.html — Build Process

How a dead 2010 Blogger widget got diagnosed and rebuilt · Claude (Sonnet 5) · 2026-07-29


1 · The task

The user had a "related posts" widget running on a Blogger site for about ten years that had stopped working, and asked for it to be upgraded using a current Google API. The only lead was a URL to the original 2010 source post: Hoctro's blog entry "Tiện ích mới: Viết liên quan (Related)" at hoctroviet.blogspot.com.

2 · Diagnosing why it broke

Rather than guess, the original post was fetched and the exact code it shipped was extracted verbatim (via WebFetch). The script did three things that no longer work on the modern web:

  1. Called google.load('search', '1.0') and instantiated google.search.SearchControl / google.search.WebSearch — the Google AJAX Search API. Google shut this API down in November 2014. Every call into google.search.* since then is a no-op or a silent failure, which explains why the widget just stopped rendering with no visible error.
  2. Loaded its loader script from http://www.google.com/jsapi — the API-loader endpoint tied to that same discontinued API family.
  3. Loaded jQuery and the loader over plain http://, which modern browsers block as mixed content on an https://-served Blogger blog, independent of the API being alive or not.

This wasn't a syntax bug or a selector that drifted — the entire platform the script stood on had been decommissioned. No patch to the existing code could fix it; it needed a different data source.

3 · Choosing the replacement API

Two candidate replacements exist for "search my own blog's posts, scoped by label, to find related articles":

  • Google Programmable Search Engine / Custom Search JSON API — the direct successor to the dead AJAX Search API. Closest in spirit to the original ("search the web, restricted to my site"), but requires creating a Search Engine ID (cx) and an API key in Google Cloud Console, and is capped at 100 free queries/day before billing.
  • Blogger's own JSON feed, filtered by label (/feeds/posts/default/-/LabelName?alt=json-in-script&...) — a feed Blogger has never removed, requires no API key, no Cloud Console project, and no quota, because it's simply the blog's own content exposed as JSON, not a third-party search service.

Asked the user directly which to build (AskUserQuestion), along with a second question about what "4 files" should mean given this project's usual convention (essay-style Main+Process .md/.html pairs) is unusual for a code deliverable. The user chose the Blogger-feed approach and confirmed the Main+Process convention.

4 · Rebuilding the widget

The replacement keeps the original's actual behavior — read the current post's labels, then look up other posts sharing those labels — but re-implements the mechanics:

  • No jQuery. The original used jQuery 1.3 from ajax.googleapis.com purely for $(document).ready() and a .each() loop over label links; both are trivially vanilla JS (DOMContentLoaded, a for loop over querySelectorAll results). Dropping the dependency also removes exposure to Google Hosted Libraries' own deprecation trajectory.
  • Label scraping kept, made theme-agnostic. The original hard-coded one selector (div.post-footer-line span.post-labels a) specific to Hoctro's 2010 theme. The rebuild tries a short list of selectors used by different Blogger theme generations (.post-labels a, .post-footer-line .labels a, a[rel="tag"], a[href*="/search/label/"]) and uses the first one that matches, since label markup varies by theme and the user's current theme is unknown.
  • JSONP against Blogger's own feed, one request per label (capped at 4 labels), each with its own dynamically-named global callback (hoctroRelatedCB_N) so concurrent requests don't clobber a shared callback. Using alt=json-in-script (script-tag JSONP) rather than fetch() against alt=json sidesteps any dependency on Blogger's feed CORS headers — a script tag load is immune to CORS by construction.
  • Client-side merge/dedupe across the per-label result sets, excluding the current post's own URL, capped at 8 displayed results — replicating what the original's single web-search call did in one round-trip, just assembled from several feed calls instead.
  • Graceful empty states: if the current page has no label links at all (front page, label archive page, etc.), the widget reports that plainly instead of showing a spinner forever — the original had no such fallback and would hang silently in Loading... off a dead API.

5 · Obstacles hit and the exact fix for each

Four separate problems surfaced while producing these files — one in the reused MD→HTML converter, one a Windows console quirk, one a second converter escaping gap, and one a real defect in the widget's own JS. Each is recorded here with the verbatim code change, not just a description, so a future session hitting the same symptom can find the fix without re-deriving it.

Obstacle 1 — convert_md_to_html.py had no support for fenced ` code blocks. The widget doc needed a large HTML/JS listing inside a fenced block; the existing converter only handled single-backtick inline code, so the whole listing was falling through to the "regular paragraph" branch and getting mangled. Fix — added fenced-block detection to the main parsing loop in convert_md_to_html.py, right after the horizontal-rule check:

        # Fenced code block ```lang ... ```
        if stripped.startswith('```'):
            i += 1
            code_lines = []
            while i < len(lines) and lines[i].strip() != '```':
                code_lines.append(lines[i])
                i += 1
            i += 1  # skip closing ```
            html_parts.append('<pre><code>' + escape_html('\n'.join(code_lines)) + '</code></pre>')
            continue

and a matching <pre>/<pre> code/code block added to the CSS string constant (monospace, light-grey background, overflow-x: auto so long JS lines scroll instead of breaking the 900px page width).

Obstacle 2 — no escape_html() helper existed, so Obstacle 1's fix had nothing to call. Fix — added, right before process_inline:

   

            def escape_html(text):
    """Escape HTML special characters for use inside <pre><code>."""
    return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
    
    

Obstacle 3 — inline single-backtick code spans were not escaping their content, a pre-existing gap in convert_md_to_html.py unrelated to the new fenced-block feature. The prose in RelatedPosts_Widget.md §2 contains `

The "Related Posts" Widget for Blogger — 2026 Upgrade

Replacing the Google AJAX Search API (discontinued since 2014) with the Blogger JSON Feed API · Claude (Sonnet 5) · 2026-07-29


Friends, I tried to reuse the "Related Articles" widget I wrote back in 2010, and it doesn't run anymore. So I asked Anthropic's Claude Sonnet AI (the "weakest" of the trio — Sonnet, Opus, and Fable) — and it solved it in about 10 minutes!!! I'm passing along its solution here for you to see. I haven't written JavaScript in over 10 years, so there's no way I could have upgraded this myself without relearning it from scratch — but thanks to AI, it's done, and you're welcome to install it on your own blog if you like. Hoctro, 7/29/2026. Everything from here to the end of the post was written by Claude Sonnet!


1 · Why the old (2010) code no longer runs

The original script ("Related Articles - Take 5 - Using JQuery & Google Search API" by Hoctro, August 2010; see the original blog post) relies on three things that Google has since discontinued, or that modern browsers now block:

  1. google.load('search', '1.0') and google.search.WebSearch — this is the Google AJAX Search API, an embeddable web-search API that Google officially shut down in November 2014. Since that date, every call to google.search.* either errors out or silently does nothing — this is the main reason the widget "died" without any clear error message ever appearing on the page.
  2. http://www.google.com/jsapi — the jsapi loader for the "AJAX APIs" family (Search, the old Maps, Feeds, etc.) was discontinued in the same wave as the AJAX Search API.
  3. Loading the script over http:// (not https://) — because Blogger has served every blog over HTTPS for years now, modern browsers block "mixed content" (an https page loading an http script) by default. Even if the Google AJAX Search API were still alive, that <script src="http://..."> line would still be silently blocked in most browsers today.

In short: this isn't a syntax bug that can be patched — the entire API platform this code stood on has been removed. The only way to bring the widget back to life is to build it on a different data source that's still active.

2 · The replacement: use Blogger's own built-in JSON feed

(If your page is about the songwriter Lê Uyên Phương, the widget will go find related posts.)

Rather than hunting down a new "Google Search API" (Google now offers the Programmable Search Engine / Custom Search JSON API, but it requires creating a Search Engine ID and an API key in Google Cloud Console, and is only free for 100 calls/day before billing kicks in), this upgrade uses something Blogger already provides for free, with no limits and no extra sign-up: the blog's own JSON feed filtered by label.

Every Blogger blog has this endpoint:

https://YOUR-BLOG-NAME.blogspot.com/feeds/posts/default/-/LabelName?
alt=json-in-script&max-results=6&callback=yourFunctionName

This endpoint is not the dead Google AJAX Search API — it's Blogger's own native feed (in the old GData-style JSON format, with text fields wrapped in {"$t": "..."}), and Blogger has never removed it, because countless "Popular Posts" widgets and current Blogger themes still quietly rely on this exact mechanism. Advantages over the 2010 version:

  • No API key needed, no sign-up, no quota — because this is your own blog's public feed, not a third-party search service.
  • alt=json-in-script loads via a <script> tag (the JSONP technique) — so it's completely unaffected by CORS, and works even on a custom domain mapped to Blogger.
  • No dependency on jQuery, no dependency on ajax.googleapis.com (a library that is itself on Google's own gradual deprecation path) — plain vanilla JavaScript.
  • Same logic as the original: read the label(s) of the post currently being viewed, then find other posts in the same blog sharing that label — the only difference is that it queries Blogger's own post archive directly instead of going through Google's web search results.

3 · Full code — paste into Blogger

Go to Layout → Add a Gadget → HTML/JavaScript, and paste the entire block below. The widget is designed to sit right below the post content (on the single-post "item page" view); it checks itself and hides if you're on the home page or a label page (where there's no "current" post to find anything related to).

   


<!-- ===================================================================
     Related Posts — 2026 upgrade
     Replaces the discontinued (11/2014) Google AJAX Search API with
     Blogger's own label-based JSON feed. No API key required.
     Based on the original idea: Hoctro, "Related Articles - Take 5", 8/2010
     https://hoctroviet.blogspot.com/2010/08/tien-ich-moi-viet-lien-quan-related.html
     =================================================================== -->
<div id="hoctro-related-wrap">
  <div id="hoctro-related-results">Loading related posts…</div>
</div>

<style>
#hoctro-related-wrap { margin: 24px 0; }
#hoctro-related-heading { font-size: 1.05em; font-weight: bold; margin-bottom: 8px; }
#hoctro-related-results { font-size: 0.95em; }
#hoctro-related-results ul { list-style: none; margin: 0; padding: 0; }
#hoctro-related-results li { padding: 4px 0; border-bottom: 1px dotted #ccc; }
#hoctro-related-results li:last-child { border-bottom: none; }
#hoctro-related-results a { text-decoration: none; }
#hoctro-related-results a:hover { text-decoration: underline; }
#hoctro-related-results .hr-thumb { display:inline-block; width:16px; height:16px;
vertical-align:middle; margin-right:6px; border-radius:2px; } </style> <script> (function () { 'use strict'; var MAX_LABELS_TO_QUERY = 4; // max number of labels to query (limits the number of requests) var MAX_RESULTS_SHOWN = 8; // max number of related posts to display var callbackCounter = 0; var pendingRequests = 0; var collected = []; // {title, href, thumb} var seenHrefs = {}; var currentHref = location.href.split('#')[0].split('?')[0]; function baseUrl() { return location.protocol + '//' + location.host; } function escapeHtml(s) { return String(s) .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;'); } // The current post only has "related posts" on the item page (single-post view). // Different Blogger themes set the "item-page" class or body data differently, // so the safest approach is: only run once we find exactly one post block // and at least one label link on the page. function getCurrentLabels() { var labels = []; var seen = {}; // Different Blogger themes place label links in different spots/classes; // probe them in order and use the first selector that matches. var selectors = [ '.post-labels a', '.post-footer-line .labels a', 'a[rel="tag"]', 'a[href*="/search/label/"]' ]; for (var s = 0; s < selectors.length; s++) { var links = document.querySelectorAll(selectors[s]); if (links.length) { for (var i = 0; i < links.length; i++) { var href = links[i].getAttribute('href') || ''; var m = href.match(/\/search\/label\/([^?&#\/]+)/); if (m) { var label = decodeURIComponent(m[1].replace(/\+/g, ' ')); if (!seen[label]) { seen[label] = true; labels.push(label); } } else if (!href) { var text = (links[i].textContent || '').trim(); if (text && !seen[text]) { seen[text] = true; labels.push(text); } } } break; // the first selector that returns results is enough — stop probing } } return labels; } function renderResults() { var box = document.getElementById('hoctro-related-results'); var heading = document.getElementById('hoctro-related-heading'); if (!collected.length) { box.innerHTML = '<em>No related posts found.</em>'; return; } var html = '<ul>'; for (var i = 0; i < Math.min(collected.length, MAX_RESULTS_SHOWN); i++) { var item = collected[i]; var thumb = item.thumb ? '<img class="hr-thumb" src="' + escapeHtml(item.thumb) + '" alt="">' : ''; html += '<li>' + thumb + '<a href="' + escapeHtml(item.href) + '">' + escapeHtml(item.title) + '</a></li>'; } html += '</ul>'; box.innerHTML = html; heading.style.display = ''; } function handleFeed(feed) { pendingRequests--; var entries = (feed && feed.feed && feed.feed.entry) ? feed.feed.entry : []; for (var i = 0; i < entries.length; i++) { var entry = entries[i]; var href = ''; for (var l = 0; l < entry.link.length; l++) { if (entry.link[l].rel === 'alternate') { href = entry.link[l].href; break; } } if (!href || href === currentHref || seenHrefs[href]) continue; seenHrefs[href] = true; collected.push({ title: entry.title && entry.title.$t ? entry.title.$t : '(untitled)', href: href, thumb: entry.media$thumbnail ? entry.media$thumbnail.url : '' }); } if (pendingRequests === 0) renderResults(); } function queryLabel(label) { pendingRequests++; var cbName = 'hoctroRelatedCB_' + (callbackCounter++); window[cbName] = function (feed) { handleFeed(feed); delete window[cbName]; }; var src = baseUrl() + '/feeds/posts/default/-/' + encodeURIComponent(label) + '?alt=json-in-script&max-results=6&callback=' + cbName; var script = document.createElement('script'); script.src = src; script.async = true; document.body.appendChild(script); } function init() { var labels = getCurrentLabels(); if (!labels.length) { document.getElementById('hoctro-related-results').innerHTML = '<em>This post has no labels, so no related posts can be found.</em>'; return; } labels = labels.slice(0, MAX_LABELS_TO_QUERY); for (var i = 0; i < labels.length; i++) queryLabel(labels[i]); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); </script>

4 · Installation and customization

  1. Go to Blogger → Theme → Edit HTML, or more simply: Layout → Add a Gadget → HTML/JavaScript, then paste the entire code block from section 3.
  2. If you use the "Add a Gadget" route, the widget will appear in whichever column position you chose, on every page (home page, label page, post page) — the script checks itself and shows a "no labels" message on pages that aren't a single post. If you want it to appear only below each individual post (matching how Hoctro's original was used), the more proper approach is to edit the HTML directly: find the <data:post.body/> tag inside <b:includable id='post' var='post'> and paste the code block right after it.
  3. MAX_LABELS_TO_QUERY and MAX_RESULTS_SHOWN at the top of the script can be adjusted to taste (default: probe up to 4 labels, show up to 8 related posts).
  4. If your theme doesn't expose post labels through any of the selectors in the selectors list (see the "Different Blogger themes place label links..." comment), open DevTools (F12) on a post, find the HTML block containing the label links at the end of the post, and add your theme's actual class/selector to that array.
  5. The thumbnail image (hr-thumb) only shows up if Blogger generated a thumbnail for that post (usually automatic if the post has an image) — nothing else needs to be configured.

5 · If you still want to use the Google Custom Search JSON API

The approach above is free, unlimited, and requires no sign-up. If you later want to go back to the "search the whole web, restricted to my own site" style of the 2010 original, that route still exists under a new name — Google Programmable Search Engine (formerly called Custom Search Engine), with the Custom Search JSON API standing in for the dead AJAX Search API. Things to know before choosing this route:

  • You must create a Search Engine ID (cx) at programmablesearchengine.google.com, scoping the search to your blog's domain.
  • You must create an API key in Google Cloud Console and enable the Custom Search API.
  • Free for 100 calls/day; beyond that, billing applies per 1,000 calls.
  • The endpoint is a normal REST JSON call (https://www.googleapis.com/customsearch/v1?key=...&cx=...&q=...), made with plain fetch() — no JSONP needed.

Since a "related posts" widget only ever needs to search within your own blog (it doesn't need real web-search power), the Blogger-feed approach from sections 2–4 remains the more sensible choice for this purpose — and it's exactly the approach implemented in this upgrade.

***

RelatedPosts_Widget.md/.html — Build Process

How a dead 2010 Blogger widget got diagnosed and rebuilt · Claude (Sonnet 5) · 2026-07-29


1 · The task

The user had a "related posts" widget running on a Blogger site for about ten years that had stopped working, and asked for it to be upgraded using a current Google API. The only lead was a URL to the original 2010 source post: Hoctro's blog entry "Tiện ích mới: Viết liên quan (Related)" at hoctroviet.blogspot.com.

2 · Diagnosing why it broke

Rather than guess, the original post was fetched and the exact code it shipped was extracted verbatim (via WebFetch). The script did three things that no longer work on the modern web:

  1. Called google.load('search', '1.0') and instantiated google.search.SearchControl / google.search.WebSearch — the Google AJAX Search API. Google shut this API down in November 2014. Every call into google.search.* since then is a no-op or a silent failure, which explains why the widget just stopped rendering with no visible error.
  2. Loaded its loader script from http://www.google.com/jsapi — the API-loader endpoint tied to that same discontinued API family.
  3. Loaded jQuery and the loader over plain http://, which modern browsers block as mixed content on an https://-served Blogger blog, independent of the API being alive or not.

This wasn't a syntax bug or a selector that drifted — the entire platform the script stood on had been decommissioned. No patch to the existing code could fix it; it needed a different data source.

3 · Choosing the replacement API

Two candidate replacements exist for "search my own blog's posts, scoped by label, to find related articles":

  • Google Programmable Search Engine / Custom Search JSON API — the direct successor to the dead AJAX Search API. Closest in spirit to the original ("search the web, restricted to my site"), but requires creating a Search Engine ID (cx) and an API key in Google Cloud Console, and is capped at 100 free queries/day before billing.
  • Blogger's own JSON feed, filtered by label (/feeds/posts/default/-/LabelName?alt=json-in-script&...) — a feed Blogger has never removed, requires no API key, no Cloud Console project, and no quota, because it's simply the blog's own content exposed as JSON, not a third-party search service.

Asked the user directly which to build (AskUserQuestion), along with a second question about what "4 files" should mean given this project's usual convention (essay-style Main+Process .md/.html pairs) is unusual for a code deliverable. The user chose the Blogger-feed approach and confirmed the Main+Process convention.

4 · Rebuilding the widget

The replacement keeps the original's actual behavior — read the current post's labels, then look up other posts sharing those labels — but re-implements the mechanics:

  • No jQuery. The original used jQuery 1.3 from ajax.googleapis.com purely for $(document).ready() and a .each() loop over label links; both are trivially vanilla JS (DOMContentLoaded, a for loop over querySelectorAll results). Dropping the dependency also removes exposure to Google Hosted Libraries' own deprecation trajectory.
  • Label scraping kept, made theme-agnostic. The original hard-coded one selector (div.post-footer-line span.post-labels a) specific to Hoctro's 2010 theme. The rebuild tries a short list of selectors used by different Blogger theme generations (.post-labels a, .post-footer-line .labels a, a[rel="tag"], a[href*="/search/label/"]) and uses the first one that matches, since label markup varies by theme and the user's current theme is unknown.
  • JSONP against Blogger's own feed, one request per label (capped at 4 labels), each with its own dynamically-named global callback (hoctroRelatedCB_N) so concurrent requests don't clobber a shared callback. Using alt=json-in-script (script-tag JSONP) rather than fetch() against alt=json sidesteps any dependency on Blogger's feed CORS headers — a script tag load is immune to CORS by construction.
  • Client-side merge/dedupe across the per-label result sets, excluding the current post's own URL, capped at 8 displayed results — replicating what the original's single web-search call did in one round-trip, just assembled from several feed calls instead.
  • Graceful empty states: if the current page has no label links at all (front page, label archive page, etc.), the widget reports that plainly instead of showing a spinner forever — the original had no such fallback and would hang silently in Loading... off a dead API.

5 · Obstacles hit and the exact fix for each

Four separate problems surfaced while producing these files — one in the reused MD→HTML converter, one a Windows console quirk, one a second converter escaping gap, and one a real defect in the widget's own JS. Each is recorded here with the verbatim code change, not just a description, so a future session hitting the same symptom can find the fix without re-deriving it.

Obstacle 1 — convert_md_to_html.py had no support for fenced ` code blocks. The widget doc needed a large HTML/JS listing inside a fenced block; the existing converter only handled single-backtick inline code, so the whole listing was falling through to the "regular paragraph" branch and getting mangled. Fix — added fenced-block detection to the main parsing loop in convert_md_to_html.py, right after the horizontal-rule check:

        # Fenced code block ```lang ... ```
        if stripped.startswith('```'):
            i += 1
            code_lines = []
            while i < len(lines) and lines[i].strip() != '```':
                code_lines.append(lines[i])
                i += 1
            i += 1  # skip closing ```
            html_parts.append('<pre><code>' + escape_html('\n'.join(code_lines)) + '</code></pre>')
            continue

and a matching <pre>/<pre> code/code block added to the CSS string constant (monospace, light-grey background, overflow-x: auto so long JS lines scroll instead of breaking the 900px page width).

Obstacle 2 — no escape_html() helper existed, so Obstacle 1's fix had nothing to call. Fix — added, right before process_inline:

   

            def escape_html(text):
    """Escape HTML special characters for use inside <pre><code>."""
    return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
    
    

Obstacle 3 — inline single-backtick code spans were not escaping their content, a pre-existing gap in convert_md_to_html.py unrelated to the new fenced-block feature. The prose in RelatedPosts_Widget.md §2 contains `

7.28.2026

Duyên Hội-Ngộ Trong Câu Hò Miền Nam

Thuần-Phong

Tạp chí Bách Khoa, số 7 (1957), trang 51–56

Claude Sonnet OCR ra tiếng Việt, Học Trò đối chiếu và sửa chữa vài chỗ.


Ca-dao Với Dân-tộc

Đã từng học-hỏi ca-dao, ai cũng phải thừa-nhận tánh-cách đồng-nhứt của dân-tộc, từ văn-chương đến phong-tục, trong lãnh-vực cũng như ở tinh-thần. Ca-dao là một phương-tiện tuyên-truyền, một thể-thức liên-lạc, vượt tất cả đèo ải núi sông, qua suốt cả thời-gian triều-đại. Trải qua tất cả chướng-ngại vật-chất và vô-hình, ca-dao đi sát theo gót nam-tiến và có khi tung bay ra hải-ngoại lân-bang.

Trước hơn hết, ca-dao là chiếc xe chở ngôn-ngữ từ Bắc vào Nam; như tiếng « ô », tiếng « mồm », hai tiếng thổ-ngữ ở Bắc và ở Trung chỉ có lưu-truyền trong hai câu ca-dao miền Nam, mà không hề nói:

— Lọng che sương dầu sườn cũng lọng,
Cái « ô » bịt vàng dầu trọng cũng « ô ».
— Một trăm ông chú không lo,
Lo vì một nỗi mụ o nỏ « mồm ».

Những tư-tưởng phổ-thông, phản-ảnh khía-cạnh của dân-tộc-tính, cũng nương theo giọng lảnh-lót mà truyền thẳng vào Nam. Chẳng hạn câu ca-dao ở Bắc:

Chim quyên xuống suối ăn trùn,
Anh hùng lỡ vận lên rừng đốt than.

chuyển vào Nam thành câu:

Chim quyên xuống suối kiếm mồi,
Thấy anh lao-khổ đứng ngồi không yên.

Có những câu đi ngang qua thời-gian suốt Bắc, Trung, Nam, mà vẫn còn nguyên vẹn:

— Gió đưa cây cải về trời,
Rau răm ở lại chịu lời đắng cay.
— Chiều chiều lại nhớ chiều chiều,
Nhớ người đãy gấm khăn điều vắt vai.
— Chiều chiều lại nhớ chiều chiều,
Bâng-khuâng nhớ mợ chín chiều ruột đau.

Chủ-yếu và thông-dụng nhứt là câu cổ-võ đoàn-kết:

Một cây làm chẳng nên non,
Ba cây giụm (chụm) lại nên hòn núi cao.

Ngoài ngôn-ngữ và tư-tưởng, ca-dao vẫn còn chứng-minh được sự đồng-nhứt trong thể-cách hành-văn, trong phương-pháp diễn-tả. Câu trong Nam người bạn biển hát ở ngoài khơi hòn Phú-Quốc:

Tháng giêng là tháng ăn chơi,
Anh đi năm bãi hòn Khơi một mình.

nếu không phải vì truyền-thống đồng-nhứt, có lẽ nào ngẫu-nhiên mà trùng với câu ca-dao ở đồng quê sông Nhị-Hà:

Tháng giêng là tháng ăn chơi,
Tháng hai trồng đậu, trồng khoai, trồng cà...

Bài nầy không nhắm mục-đích chứng-minh sự thống-nhứt xứ-sở. Một ít thí-dụ dẫn-chứng, chỉ mong thanh-minh trước rằng bài nầy không có dụng-tâm phân-biệt địa-phương, dưới một đầu đề thâu-hẹp. Đầu đề sở-dĩ thâu hẹp vì vấn-đề quá ư to rộng mà nhứt là vì quan-niệm dân-tộc vẫn đồng-nhứt về vấn-đề, dầu cho xét ca-dao ở lưu-vực sông Hồng, xét câu hò mái đẩy ở mặt sông Hương, hay là xét câu huê-tình trong giòng sông Cửu. Vậy thâu hẹp vấn-đề ở trong khung-cảnh miền Nam tựu-trung không có dụng-ý phân ranh hạn giới mà trái lại, có tác-dụng điển-hình cho khắp cả dân-tộc từ Bắc chí Nam. Phương-chi, từ hồi Hoàng-ngọc-Phách trong bài diễn-văn đọc tại Hội Trí-Tri Nam-Định ngày 14-10-1923 và Phạm-Quỳnh trong bài diễn-văn đọc tại Hội Địa-Dư Hà-Nội ngày 19-12-1929, dẫn đến Hoa-Bằng Hoàng-thúc-Trâm trong quyển « Dân-Tộc-Tính trong Ca-dao », đã nhiều lần ca-dao ngoài Bắc được nghiên-cứu trình-bày, được rộng-rãi phổ-biến khắp nước; trong lúc ấy, ca-dao miền Nam chỉ được hát, hò trong cảnh đồng rộng sông dài, chưa có dịp phô-trương giới-thiệu.

Duyên Gặp-gỡ

Cùng một dân-tộc, cùng một huyết-thống, cùng một tâm-hồn, nên ở Đàng Ngoài hay ở Đàng Trong, người Việt chúng ta bao giờ cũng vẫn là người Việt:

Tới đây chẳng hát thì hò,
Chẳng phải con cò ngóng cổ mà nghe.

Vì yêu đời mà vừa làm vừa hát; vừa làm vừa hát để càng yêu đời. Từ xưa và từ Nam chí Bắc, hát gắn liền với cuộc đời, hát là một điều-kiện sanh-sống, — sanh-sống tập-đoàn: trước hơn hết, hát là duyên gặp-gỡ.

Trên chiếu bông hay trường-kỷ, ngoài điếu thuốc miếng trầu bên chung trà chén rượu, những nhà trí-thức kết duyên tao-ngộ bằng câu phú câu thơ. Kẻ chân lấm tay bùn lấy trời làm nhà, lấy nước làm chiếu, chào hỏi nhau bằng tiếng hát, câu hò và cứ hát hò mà kết duyên hội-ngộ.

Trên khúc sông rộng trời khuya, một anh trạo-phu thả chèo theo nước, lòng những lâng-lâng theo ngọn thủy-triều, khấp-khởi như những chòm sao nháy trên chín từng mây, rộn-rực như các vừng lửa đóm hai bên mé rạch, không thế nào mà chẳng cất tiếng lên hò, nhứt là khi nhìn thấy một đốm đen linh-đinh thả trước mặt, sắp khuất dạng bên doi:

Bớ chiếc thuyền loan!
Khoan khoan ngớt mái,
Đặng dãy tỏ một hai lời phải trái nghe chơi.

Dầu chưa phải một nòi tình, nhưng vẫn là người đồng-điệu, khách thương-hồ cũng sẵn-sàng tao-ngộ gầy duyên, thung-dung đối lại:

Bớ chiếc ghe sau!
Chèo mau, em đợi,
Kẻo khỏi khúc sông nầy, bờ bụi tối-tăm.

Thế là duyên gặp-gỡ đã gầy đầu.

Có khi duyên gặp-gỡ bắt đầu một cách khác.

Đây là trường-hợp một cô thương-nữ đêm khuya sông vắng, lẻ-loi một gái một thuyền, muốn tìm một bạn đường cho giải muộn, lảnh-lót hát lên đánh thức cô-bác anh-em:

Gió đưa con buồn ngủ lên bờ,
Đừng cho nó xuống nó rờ mắt tôi!

Thiên-hạ đã thức và âm-thầm thổn-thức vì câu hò có duyên; để đáp lại tiếng ho hen tằng-hắng trên bờ và để trêu-ghẹo các anh trai đồng-điệu, chị lái đò nối tiếng hò lên:

Đời phải đời thạnh-trị,
Cuộc phải cuộc văn-minh,
Kìa là gió mát trăng thanh,
Biết đâu nhơn-đạo bày tình cho vui?

Quả-nhiên, trên bờ có khách đồng-điệu ứng tiếng hò lên đáp lại:

Thùng thùng! đó nói một câu,
Đây anh chầu nửa chục,
Hễ là gạn đục,
Thì phải lóng trong;
E cho miệng chẳng in lòng,
Chớ thuyền-quyên sánh với anh-hùng mới xuê.

Thì chàng « anh hùng » này tức-khắc, hoặc là thả bộ trên bờ sông, hoặc là bơi xuống theo cô lái, đối đáp cho phỉ tình đồng-điệu, thỏa duyên tao-phùng.

Duyên gặp-gỡ đã gầy mối rong, tiếp theo là một cuộc đối-đáp, gạn đục lóng trong, thử lòng đua trí.

Anh đò đã nghe « người nghĩa » cất tiếng vọng lại « Bớ chiếc ghe sau! chèo mau em đợi », là anh rán hết nước chèo theo, miệng hò với:

Thuyền em đã nhẹ
Chèo lẹ khôn theo!
Khuyên em bớt mái, khoan lèo chờ anh!

Cô lái trước đòi không lẽ hẹp-hòi mà không đáp lại:

Đây đã chèo lơi
Đặng chờ người tri-kỷ,
Gặp mặt chuyện trò cho phỉ ước-mơ.

Cô lái đã gầy đầu, anh đò cố-nhiên vội buộc:

Bâng-khuâng bát-ngát
Nghe em hát hữu tình,
Căn-duyên tiền-định khiến đôi đứa mình gặp nhau.

Từ bây giờ là bắt đầu cuộc thi tài. Cô gái hát lên một câu đối từ-ngữ, lấy tiếng của thợ mộc mà ướm tình:

Chàng đừng có lóng trong gạn đục,
Thiếp giao tình bằng thẳng như cưa,
Giữ cho trọn nghĩa sau xưa,
Cũng như anh thợ mộc liệu vừa rập-khuôn.

Anh đò bắt buộc phải đáp lại theo phương-pháp mỹ-từ ấy, dùng tiếng thuật-ngữ của thợ dệt tơ:

Anh dốc kén cho đặng một người kim chỉ,
Nên chí làm cho phỉ tóc-tơ
Bởi vậy anh mới ước mơ,
Cũng như người dệt lụa giữ hờ mối-manh.

Cô lái tiếp-tục đối-ngữ, dùng những vật lễ-nghi trong đám cưới:

Chim chìa-vôi bay ngang đám thuốc
Cá bả-trầu lội tuốt mương cau
Đâu anh đối thử cho mau!
Em đây chịu khó đứng hầu lóng nghe.

Anh đò khéo hơn, dùng những tiếng trong bếp, hòa lẫn những tiếng văn-phòng, ngụ-ý hợp-hòa hôn-phối:

Nước chanh-giấy rưới vào mắm mực,
Rau mùi-viết trộn lộn giấm son,
Bốn mùi hiệp lại càng ngon,
Cũng như qua gần bậu, chẳng còn nhớ trông.

Cô lái chưa chịu, sang qua nghề hạ-bạc có ý mỉa-mai:

Quần lủng đáy đi đâu xớ-rớ,
Áo rách te nói chuyện bốc-chài,
Thế-gian em thấy thường hoài,
Hễ ăn được cá, tính bài bỏ nơm.

Anh đò đáp bằng nghề cung ná phỏng-đối:

Cơn ngộ-nạn gió run bây-bảy,
Lúc giựt-giàng chạy lịa băng cung,
Việc đời nói chẳng hay cùng,
Đặng chim bỏ ná, anh chẳng dùng vậy đâu.

Cô lái trở về vườn quê, ví mình với trái hạnh, biểu-hiệu đức vẹn toàn:

Thân em như trái hạnh
Rành rạnh đang tròn,
Dầu sương dầm, nắng dãi, không mòn tiết xuân.

Anh đò liền hóa thân thành con chim để đáp gần thứ trái cây tiêu-biểu cho đức-hạnh ấy:

Thân anh như con én
Léo-lén trên nhành,
Muốn kề trái hạnh, chẳng đành bay xa.

Đã cao-rao mình là gái tiết-hạnh, không lẽ dành cho én đưa oanh kề, nên cô lái biến ra một thứ hoa, tượng-trưng cho đức thanh-khiết:

Thân em như bông bưởi trắng ròng,
Tuy mùi thơm nức mũi, mà lòng sạch trong.

Hoa bưởi đã dành tượng-trưng cho thanh-khiết, nhưng chưa phải biệt-lập một cõi như Nhị-kiều trong chốn nhà vàng, mà trái lại vẫn bị chùm-gởi bám-vây, nên anh đò khéo hạ mình đáp lại:

Qua ví như chùm-gởi đáp nhờ,
Gá vô nhành bưởi nọ, đặng nhờ hưởng hơi.

Nhận biết đã sơ-hở, cô lái vẫn đề-cao giá-trị bản-ngã, vừa cảnh-cáo anh chàng, hát trớ:

Thiếp như một cụm hoa hường:
Thấy xinh mà rờ đến, mắc đường chông gai.

Anh đò tỏ ý cao-thượng, đáp:

Qua cũng ví như cái hột sương,
Rưới hoa hường cho tươi-tắn,
Vì tiếc cánh hoa lành bị nắng héo khô.

Đã bắt đầu đề-cao tài, hạnh và sắc mà vẫn bị chàng trai ăn qua, cô lái duy còn có một nước là tự tôn mình lên một ngôi-vị thiêng-liêng cao cả, may ra được kẻ đối-thủ tôn-thờ:

Thiếp như trái Phật-thủ khác gì?
Đẹp thì thấy đẹp, ăn thì khó ăn.

Nhưng không! Cuộc đời là một trường tương-đối: ma linh còn có quỉ linh hơn; quỉ linh còn có thần linh hơn; thần vẫn còn kém thánh; thánh há cao hơn tiên? Tiên chưa dễ sánh cùng Phật! Nhưng Đứng Thế-Tôn, dầu cho linh-thiêng hơn chúng-sanh vạn-vật, vẫn không thể lìa-tách cõi đời: chính có cõi đời mới có chỗ cho Thế-tôn « ngự-trị ». Anh trai mộc-mạc kia tự ví mình là chỗ ngự-trị của Thế-Tôn, là căn-bản của Đứng Chí-Linh Toàn-Giác ấy:

Qua như cái quả-tử trên bàn,
Để chưng Phật-thủ vững-vàng xem chơi!

Cô ơi! cái chi-chi ấy của cô, cô cho là linh, là « Phật » ấy, thực-tế là món đồ để trang-hoàng, để chưng-diện, để « xem chơi », nằm ì trên cái dĩa quả-tử: chuốt-ngót lời vàng mà treo cao giá ngọc, chỉ cho anh trai đồng chua nước mặn bụm miệng cười dài? Đành rằng một mai nên lứa thành đôi, có « ông thánh » nào mà khỏi « lập trang thờ bà », nhưng giờ linh chưa gá, thì cô mình chẳng qua là khăn-yếm thường-tình.

Thế mà cô vẫn đánh liều nguy-biện, còn cao giọng khoe lên:

Thân em xét kỹ
Cũng tỉ như đồng bạc đầu hình,
Người người ai cũng muốn nhìn,
Lăng-xăng đương buổi chợ, gởi mình vào đâu?

Tài năng, nhan-sắc, đức-hạnh, linh-thiêng, những giá-trị cao nhứt ở đời đã hoá ra vô hiệu-lực đối với anh chàng tay lấm chơn bùn; giờ cô chỉ còn một hi-vọng cuối-cùng, một giá-trị thực-tế, một miếng mồi ngon-lành là đồng bạc, đồng bạc trắng đã vì nó mà chú tây đen làm lụy biết bao khách má hồng! Trăm phần anh lái đò cũng phải theo luật « sáng đèn hoa mắt », « trắng bạc đen lòng » mà phải qui-hàng thúc-thủ, cô tưởng như thế.

Lầm to! cái luật « hoàng kim hắc thế tâm », có đúng là đúng cho những người bất-nghĩa. Còn anh chàng quê-mùa chất-phác, ăn chắc mặc dày, có tham tiền là tham đồng tiền « phải nghĩa », đồng tiền mồ hôi nước mắt của mình làm ra, và cất kỹ đồng tiền của mình trong chỗ chắc-chắn. Cho nên anh đối lại:

Thân qua xét chắc
Như tủ sắt để hờ.
Góp thâu tiền-bạc giấy-tờ,
Đồng nào phải nghĩa, anh được nhờ, cất vô.

Thế là hết rồi! Còn gì cao-quí mà khoe-khoang? Nên cô lái đò cứ im-lặng mà chèo xuôi theo nước. Anh trai còn luyến-tiếc không muốn cho dứt cuộc hát hò, nên trở lại « tấn-công »:

Canh hãy còn khuya,
Đàng về thâm-thầm,
Đôi ta xứng lắm,
Như kép gặp đào.
Hát chơi cho giải muộn, lẽ nào làm ngơ?

Tiếc thay mãn nước tàn canh, bầy gà đồng-thanh gáy sáng, giục-giã đôi bạn phân-lìa. Cô gái cất tiếng lên từ-tạ:

Đêm lụn canh tàn,
Giã chàng ở lại,
Ra về hoằn-hoại,
Luống những đau thương,
Chào nhau cách mặt đôi đường,
Dứt câu hò hát, lo-lường bán buôn.

Để chấm dứt buổi tao-phùng, anh trai cũng hò theo một câu tạm biệt:

Gió thổi hiu-hiu,
Chín chiều ruột thắt,
Nhìn sao bên Bắc,
Nước mắt chảy bên Đông,
Ai xui chi cho vợ vợ chồng chồng,
Không biết đây với đó, dây tơ hồng có xe?

Cuộc gặp-gỡ giữa trai gái như trên đây là một sự việc thường-trực, diễn ra hằng ngày, hằng đêm, trên đồng, dưới sông, chung-quanh cối xay, cối giã, giữa đêm trăng, trong đêm tối, giữa một gái một trai, giữa một đám người có thể gồm cả nam phụ lão ấu, trong hoàn-cảnh thiên-nhiên theo nhịp sống thông-thường của dân-tộc. Hoàn-cảnh thiên-nhiên sắp bày cho cuộc gặp-gỡ dượm sắc-màu của đất nước; dân-tộc không cho cuộc gặp-gỡ như thế là trái ngược với đạo-lý luân-thường, tuy câu châm-ngôn đã cấm: « Nam nữ thọ thọ bất thân ». Câu nầy chẳng qua là một hàng rào thưa ngăn-đón hạng trí-thức và phú-hào, một thiểu-số dễ bề truy-lạc vì mảng ăn không ngồi rồi. Sanh-hoạt trong nông-thôn, công-tác ở nơi đồng-áng, đã không phân rẽ gái trai, mà trái lại, luôn luôn đoàn-tụ, đoàn-tụ trong một tinh-thần tương-trợ, tương-thân, trên một thửa đất màu-mỡ lành-mạnh: không có một câu ca-dao thô-tục lưu-manh, chỉ toàn là những câu thanh-tao đạo-lý; đó là một bằng-chứng hùng-hồn làm chứng cho tinh-thần lành-mạnh của gái trai thôn-quê, làm chứng cho những cuộc gặp-gỡ duyên-vị nồng-nàn, mà không có mảy-may ngụy-tà tội-lỗi.

THUẦN-PHONG

7.27.2026

Lê Uyên Phương — "Từ đôi": khảo sát từ láy / từ ghép song tiết trong lời ca

Bạn,

Tôi có 21 lời bài nhạc Lê Uyên Phương trong hai tập ca khúc của Ông. Tôi thử coi Claude AI Sonnet (cái yếu nhất và cũng rẻ tiền nhất) có thể tìm ra các từ kép được hay không, thì nó giải bài toán này trong 1/2 giờ!!!

Tôi gửi bạn kết quả, cũng như cách nó tạo ra (process).

Mỗi khi làm một cái gì, tôi hay biểu nó làm như vậy: mày ghi xuống cho tao những gì mày làm. để tao còn học hỏi theo! Thế là nó nghe lời răm rắp. Mời bạn xem cả hai bài, có nhiều chi tiết chắc sẽ làm bạn thích thú với ca từ Lê Uyên Phương.

Thân ái,

Học Trò

(7/27/2026)

Ghi chú: từ đây tới cuối bài là do AI viết, kể cả nghĩa của từ láy (cột Nghĩa (từ điển))!!!



Ý tưởng: nhiều từ tiếng Việt chỉ có nghĩa khi đi thành cặp hai âm tiết — tách riêng từng chữ thì vô nghĩa hoặc nghĩa khác hẳn. Ví dụ do chính người yêu cầu khảo sát nêu ra: "vòng tay", "dung nhan", "huy hoàng". Bài này dùng kho ngữ liệu 21/21 bài Lê Uyên Phương của hai tập Yêu Nhau Khi Còn ThơKhi Loài Thú Xa Nhau (bài gốc dùng 20/21 bài thống kê ở _LeUyenPhuong_WordUsage_Analysis.md — file đó chưa cập nhật đợt bổ sung này, xem ghi chú cuối mục 0), đếm cặp âm tiết liền kề (bigram) thay vì từng âm tiết đơn lẻ, để tìm ra những "từ đôi" nào lặp lại đủ thường xuyên và đủ "dính" với nhau (so với xác suất ngẫu nhiên) để được coi là một từ ghép/từ láy thật sự, chứ không phải hai chữ tình cờ đứng cạnh nhau.

Cách đo độ "dính" gọi là PMI (pointwise mutual information — thông tin tương hỗ theo điểm); cách tính cụ thể, với đúng hai ví dụ "vòng tay""dung nhan"/"huy hoàng" người yêu cầu đưa ra, nằm ở file _LeUyenPhuong_TuGhep_Process.md đi kèm — file đó cũng ghi lại toàn bộ đợt tính lại 2026-07-27 (bổ sung bài 21 + sửa lỗi đếm thiếu phát hiện được khi làm việc này). Ở đây chỉ trình bày kết quả dưới dạng bảng đếm — không dựng lại nguyên văn câu hát nào.

0. Hai tập nhạc và 21 bài

Tập 1 — Yêu Nhau Khi Còn Thơ (1960–1967), 10 bài: Buồn Đến Bao Giờ · Kỷ Niệm Trong Chiều · Nỗi Buồn Dâng Hiến · Một Ngày Vui Mùa Đông · Đêm Chợ Phiên Mùa Đông · Còn Nắng Trên Đồi · Bài Ca Hạnh Ngộ · Hết Rồi Những Ngày Vui · Bên Hồ Than Thở · Một Dạ Hội Buồn.

Tập 2 — Khi Loài Thú Xa Nhau (1967–1969), 12 bài: Vũng Lầy Của Chúng Ta · Chiều Phi Trường · Không Nhìn Nhau Lần Cuối · Cho Lần Cuối · Hãy Ngồi Xuống Đây · Tình Khúc Cho Em · Dạ Khúc Cho Tình Nhân · Buồn Đến Bao Giờ (bản in lại y hệt bài #1 tập 1) · Lời Gọi Chân Mây · Đưa Người Tuyệt Vọng · Ngồi Lại Trên Đồi · Đá Xanh.


Cộng hai tập là 22 lượt tên bài; vì "Buồn Đến Bao Giờ" ở tập 2 là bản in lại y hệt bài tập 1 (không phải bài khác), số bài khác nhau thực sự là 21. Kho ngữ liệu dùng cho các bảng dưới đây nay gồm đủ 21/21 bài — "Tình Khúc Cho Em" (bản TinhKhucChoEm_KyAm.md, 173 âm tiết lời) đã được bổ sung vào đợt tính lại này.

Hai chỉnh sửa so với bản gốc (2026-07-27):

  1. Bổ sung bài 21 — "Tình Khúc Cho Em". Đóng góp riêng: 173 âm tiết, 172 bigram nội bộ. Cụm nổi bật nhất bài này là "nồng nàn" (4 lần, chỉ trong bài này — xem mục 2 và mục 6).
  2. Sửa lỗi đếm thiếu ở bài "Lời Gọi Chân Mây". Khi dựng lại corpus để cộng thêm bài 21, đối chiếu số âm tiết/bài với _LeUyenPhuong_WordUsage_Analysis.md cho thấy bài này trước đây chỉ được đếm 70/181 âm tiết thật — bản gốc chỉ bắt được cột "Khổ 1", bỏ sót cột "Khổ 2 (cùng nhạc)" (63 âm tiết) và đoạn hội tụ "Lời (chung 2 khổ)" cuối bài (48 âm tiết), do file này có cấu trúc bảng đặc biệt (2 khổ song song rồi hội tụ thành 1 dòng — xem mục cấu trúc trong chính file ký âm). Đã tính lại đầy đủ 181 âm tiết cho bài này. Chi tiết cách phát hiện và sửa nằm ở file Process.

Vì cả hai thay đổi trên làm tăng mẫu số (tổng âm tiết/bigram toàn kho), mọi con số PMI trong bài này (kể cả hai ví dụ gốc "vòng tay"/"dung nhan"/"huy hoàng") đều xê dịch nhẹ so với bản trước — xem mục 5.

1. Tổng quan

  • Tổng số âm tiết lời ca đã trích: 3.952 (trước: 3.661)
  • Tổng số cặp âm tiết liền kề (bigram) hình thành: 3.923 (trước: 3.636)
  • Số cặp khác nhau: 2.878 (trước: 2.712), trong đó phần lớn (khoảng 80%) chỉ xuất hiện đúng 1 lần — dấu hiệu cho thấy Lê Uyên Phương không lặp lại cụm từ máy móc, trừ những cụm mang chức năng "từ đôi cố định" thật sự, vốn sẽ nổi lên rõ trong bảng PMI dưới đây.

2. Các "từ đôi" nổi bật nhất theo PMI (đã lọc còn 58 cặp, xuất hiện ≥2 lần)

Bảng dưới xếp theo độ "dính" (PMI) giảm dần, chỉ giữ những cặp thực sự là từ láy hoặc từ ghép (đã loại các cụm ngữ pháp thuần túy như "em ơi", "một lần", "có ai", "bao nhiêu", các cụm là điệp khúc/tên bài/hình ảnh riêng của một bài lặp lại — như "ngồi xuống đây", "viên đá", "cầm chắc", "dang đôi", "chia phôi", "thềm ga", "phố trưa" — và các cụm hô ngữ/vocalise như "ô hay", "la la"). Bảng này dài hơn bản trước (40→58 cặp) — không phải vì tiêu chuẩn lọc nới lỏng, mà vì kho ngữ liệu đã lớn hơn thật (thêm bài 21 + sửa lỗi đếm thiếu ở "Lời Gọi Chân Mây", xem mục 0), nên có thêm bằng chứng thống kê cho nhiều cặp mới. Cột "Nghĩa" là định nghĩa từ điển thông thường, không trích từ bài hát nào. Các dòng đánh dấu (mới) là những cặp không có trong bảng gốc.

Từ đôi Loại Nghĩa (từ điển) Số lần PMI
sum vầy Hán-Nôm ghép họp mặt đông đủ 2 10.96
kinh kỳ Hán-Việt kinh đô, chốn phồn hoa 2 10.96
bối rối từ láy lúng túng, không biết xử trí 2 10.96
tê tái từ láy đau nhói, tê buốt trong lòng 2 10.96
vuốt ve từ láy vuốt nhẹ, âu yếm 2 10.96
mênh mông từ láy rộng lớn không bờ bến 2 10.96
rộn ràng từ láy náo nức, tưng bừng 2 10.96
đậm đà (mới) từ láy sâu sắc, nồng nàn 2 10.96
vũng lầy từ ghép thuần Việt chỗ đất sụt, bùn lầy 2 10.96
ngao ngán từ láy chán nản, ngán ngẩm 2 10.96
kỷ niệm Hán-Việt điều còn nhớ lại từ quá khứ 3 10.37
ê chề từ láy nhục nhã, mỏi mệt ê ẩm 3 10.37
đơn côi Hán-Việt/bán Hán lẻ loi, không nơi nương tựa 2 10.37
đắm đuối từ láy say mê hết mình 2 10.37
lẻ loi (mới) từ láy cô đơn, một mình 2 10.37
đắng cay từ ghép thuần Việt khổ sở, chua xót 2 10.37
ngọt ngào từ láy dịu dàng, êm ái 2 10.37
cô liêu Hán-Việt vắng vẻ, hiu quạnh 2 10.37
ngỡ ngàng từ láy bỡ ngỡ, sững sờ 3 9.96
vấn vương từ láy vướng bận, khó dứt 2 9.96
lạnh lùng từ láy thờ ơ, lạnh nhạt 2 9.96
dương trần Hán-Việt cõi trần gian, cõi sống 2 9.79
tương lai Hán-Việt thời gian sắp tới 4 9.64
lo âu từ ghép Hán-Nôm lo lắng, bất an 3 9.64
giã từ từ ghép Hán-Nôm chia tay, từ biệt 2 9.64
vắng tanh từ láy mức độ vắng hoàn toàn 3 9.37
rã rời từ láy mệt mỏi rã rượi 3 9.37
thủy chung Hán-Việt trước sau như một 2 9.37
phôi pha từ láy phai nhạt dần 2 9.37
điên cuồng từ ghép Nôm-Hán mất lý trí, quá độ 2 9.37
da thịt từ ghép thuần Việt thân thể con người 2 9.37
nghèo nàn (mới) từ láy thiếu thốn, đơn điệu 2 9.37
mịt mờ từ láy mờ tối, không rõ 2 9.37
thời gian Hán-Việt khoảng thời gian trôi qua 7 9.15
vời vợi từ láy xa xăm, cao vợi 2 9.15
trái tim (mới) từ ghép thuần Việt quả tim, biểu tượng tình cảm 2 9.15
mối duyên (mới) từ ghép Hán-Nôm duyên phận, mối tình 2 9.05
niềm tin từ ghép sự tin tưởng 3 8.90
thì thầm từ láy nói nhỏ, rỉ tai 2 8.79
gót chân từ ghép thuần Việt phần sau bàn chân 2 8.79
thơ ngây từ ghép thuần Việt ngây thơ, trong trắng 5 8.70
nụ cười từ ghép thuần Việt hình ảnh miệng cười 4 8.64
giọt nước từ ghép thuần Việt đơn vị nhỏ chất lỏng 2 8.64
âm thầm từ ghép Hán-Nôm lặng lẽ, không phô bày 5 8.53
vai gầy từ ghép thuần Việt vai gầy guộc 2 8.37
đê mê từ láy/Hán-Nôm say sưa, ngất ngây 2 8.37
mong manh (mới) từ láy yếu ớt, dễ vỡ 2 8.26
chia lìa (mới) từ ghép Hán-Nôm đồng nghĩa chia cắt, xa cách 3 8.22
nồng nàn (mới — từ bài 21) từ láy đậm đà, tha thiết 4 8.20
tháng năm (mới) từ ghép thuần Việt năm tháng, thời gian 2 8.15
mưa bão (mới) từ ghép thuần Việt mưa to gió lớn 2 8.15
thanh xuân (mới) Hán-Việt tuổi trẻ 2 8.15
dòng nước (mới) từ ghép thuần Việt dòng chảy của nước 3 7.83
ngời sáng (mới) từ ghép thuần Việt sáng rực rỡ 2 7.83
giấc mơ từ ghép thuần Việt sự mơ mộng lúc ngủ 5 7.79
dài lâu (mới) từ ghép thuần Việt lâu dài, bền vững 2 7.79
khung trời (mới) từ ghép thuần Việt bầu trời, không gian 3 7.71
thiết tha từ láy tha thiết, nồng nhiệt 11 7.70

Vài cặp lệch khỏi mẫu chuẩn, cần dò lại (†): ngón suông, lai mịt, với trái, sô có, đầu phận, dài phút xuất hiện trong dữ liệu thô với PMI cao nhưng nhiều khả năng là mảnh vỡ do bảng ký âm ngắt câu giữa hai ô (ví dụ "lai" cuối ô này + "mịt" đầu ô sau, vốn thuộc hai cụm khác nhau như "tương lai" và "mịt mờ" bị nối nhầm) chứ không phải từ ghép thật. Đợt này phát hiện thêm một trường hợp tương tự: "ù ù" (Nỗi Buồn Dâng Hiến, "…mưa nắng sớm chiều ù ù…") — chưa rõ đây là láy âm thanh (kiểu vần vè) hay chỉ là cách ngân/luyến giọng ghi lại thành hai âm "ù", nên cũng xếp vào diện cần dò lại thay vì đưa vào bảng chính. Riêng cụm "la la"/"la là"/"là la" (Bên Hồ Than Thở, PMI cao nhưng đều bị loại khỏi bảng) là vocalise — hát nốt không lời kiểu "la la…" — không phải từ vựng, nên xếp khác hẳn diện "cần dò lại": đây là điều chắc chắn, không phải điều còn nghi ngờ.

3. Những "từ đôi" lặp lại nhiều nhất bất kể PMI (tần suất thô, ≥5 lần)

Nhóm này cho thấy những cụm từ ghép/láy Lê Uyên Phương dùng đi dùng lại xuyên suốt 21 bài — tức không chỉ là từ ghép có thật, mà còn là từ vựng ruột của ông:

Từ đôi Loại Số lần
thiết tha từ láy 11
xót xa từ láy 9
mãi mãi từ láy toàn phần 7
bàn tay từ ghép thuần Việt 7
thời gian Hán-Việt 7
thương đau từ ghép đồng nghĩa 7
ái ân Hán-Việt 6
mùa xuân từ ghép thuần Việt 6
nước mắt từ ghép thuần Việt 6
mong chờ từ ghép đồng nghĩa 5
giấc mơ từ ghép thuần Việt 5
thơ ngây từ ghép thuần Việt 5
âm thầm từ ghép Hán-Nôm 5
đôi tay bán ghép (số từ + DT) 5
đớn đau gần-láy 5
yêu thương (mới) từ láy/ghép đồng nghĩa 5

4. "Từ đôi" đặc trưng riêng theo từng bài

Cũng dùng cách tính tỷ trọng-so-với-nền như bài word-usage trước, nhưng áp cho cặp âm tiết: mỗi bài, những cặp nào có tỷ trọng trong bài cao vượt hẳn mức nền toàn tập (xuất hiện ≥2 lần trong bài đó). Cột này hiện nhiều cụm là cụm ngữ pháp/điệp khúc riêng của bài (không phải từ ghép cố định trong từ điển) — điều đó tự nó là một phát hiện: Lê Uyên Phương hay xây bài trên một cụm-khóa lặp lại nhiều lần (kiểu điệp từ, đã thấy ở "Hãy Ngồi Xuống Đây" hay "Đá Xanh" khi phân tích ký âm), chứ không chỉ nghiêng về từ ghép có sẵn trong từ điển. Hai dòng cuối bảng là bài mới/bài đã sửa của đợt này.

Bài hát Cặp âm tiết đặc trưng (số lần)
Bài Ca Hạnh Ngộ như khi (2), một khi (2), khi trao (2), rồi như (4)
Bên Hồ Than Thở sum vầy (2), nơi nầy (3), yêu trong (3)
Buồn Đến Bao Giờ đến bao (4), thương đến (2), buồn ơi (2)
Chiều Phi Trường dương trần (2), ngày hôm (2), xin hãy (2)
Cho Lần Cuối cầm chắc (2), không còn (2), thấy nhau (2)
Còn Nắng Trên Đồi chiều nào (6), chiều nầy (6), dang đôi (4), tay ôm (4)
Dạ Khúc Cho Tình Nhân sao trời (2), mưa bão (2), yêu nhau (2)
Đá Xanh viên đá (5), như viên (4), sầu sầu (3)
Đêm Chợ Phiên Mùa Đông phôi pha (2), trong bóng (2), bóng đêm (2)
Đưa Người Tuyệt Vọng áo trắng (2), tìm đâu (2), cuộc tình (2)
Hãy Ngồi Xuống Đây hãy ngồi (13), ngồi xuống (13), xuống đây (13)
Hết Rồi Những Ngày Vui màu tang (2), mây chiều (3), em ơi (3)
Không Nhìn Nhau Lần Cuối thôi đành (4), xa nhau (3), đành giã (2), giã từ (2)
Kỷ Niệm Trong Chiều chiều chiều (2), xa vời (2)
Lời Gọi Chân Mây (đã sửa — nay tính đủ khổ 1+2) xin em (6), xin anh (4), yêu đương (3), em ơi / anh ơi (điệp mở mỗi câu)
Một Dạ Hồi Buồn hai đứa (2), áo xanh (2), đèn xanh (2)
Một Ngày Vui Mùa Đông ô hay (4), sao mà (4), thềm ga (2)
Ngồi Lại Trên Đồi vẫn còn (4), xa rồi (4), còn như (2)
Nỗi Buồn Dâng Hiến buồn vì (4), tương lai (4), xưa ấy (2)
Tình Khúc Cho Em (mới) nồng nàn (4), thương em (4), yêu em (4)
Vũng Lầy Của Chúng Ta còn vùi (4), xuống phố (2), phố trưa (2), cơn say (2)

5. Hai ví dụ đúng như người yêu cầu nêu ra

  • "vòng tay" — xuất hiện 2 lần trong toàn kho, cả hai lần "vòng" đều đi liền "tay" (PMI = 7,37, trước là 7,26 — xê dịch nhẹ vì mẫu số toàn kho tăng, không phải vì tần suất đổi) — dù "tay" là một trong những từ phổ biến nhất toàn tập (24 lần, không đổi — "vòng tay" không xuất hiện ở bài mới hay ở phần vừa sửa của "Lời Gọi Chân Mây"), "vòng" chỉ xuất hiện đúng 2 lần và luôn ở dạng "vòng tay" — một minh chứng rõ cho từ ghép thuần Việt (từng chữ có nghĩa riêng nhưng ghép lại mang nghĩa cụ thể hơn: "cái ôm bằng hai tay").
  • "dung nhan""huy hoàng" — mỗi cặp chỉ xuất hiện đúng 1 lần trong toàn kho 21 bài (PMI dung nhan ≈ 11,96, huy hoàng ≈ 10,96 — cả hai vẫn rất cao dù mẫu số tăng, vì "dung", "nhan", "huy" đều vẫn chỉ xuất hiện đúng 1 lần mỗi âm mỗi tiếng trong toàn kho 21 bài). Cả hai là từ ghép Hán-Việt cổ điển: từng âm tiết riêng ("dung", "nhan", "huy", "hoàng") gần như không dùng độc lập trong tiếng Việt hiện đại — đúng loại "từ đôi vô nghĩa khi tách rời" mà câu hỏi ban đầu nêu ra.
  • Riêng "hoàng" thì không còn đúng tuyệt đối nữa kể từ khi thêm bài 21: "hoàng" nay xuất hiện 2 lần trong toàn kho — 1 lần trong "huy hoàng" (Buồn Đến Bao Giờ), 1 lần trong "bàng hoàng" (Tình Khúc Cho Em: "…dù tháng năm buồn vui bàng hoàng…", một từ láy khác hẳn nghĩa, "sững sờ, choáng váng"). Đây là minh hoạ đẹp cho giới hạn thống kê đã nêu ở mục 6 file Process cũ: với mẫu càng nhỏ, một từ càng dễ trông như "chỉ đi kèm một đối tác duy nhất" — thêm dữ liệu (dù chỉ 1 bài) đã đủ để phá vỡ điều đó cho "hoàng", dù chưa phá vỡ cho "dung"/"nhan"/"huy". Chi tiết cách tính PMI cho các cặp này (kể cả khi tần suất chỉ bằng 1) nằm trong file Process.

6. Nhận xét

  • Trong 58 "từ đôi" ở bảng mục 2, khoảng 25 cặp (43%) là từ láy (bối rối, ngọt ngào, mênh mông, rộn ràng, đắm đuối, lạnh lùng, rã rời, mịt mờ, thì thầm, thiết tha, mong manh, nồng nàn, nghèo nàn, lẻ loi…), khoảng 16 cặp (28%) là Hán-Việt hoặc Hán-Nôm ghép (kỷ niệm, tương lai, thời gian, thủy chung, âm thầm, cô liêu, dương trần, mối duyên, chia lìa, thanh xuân…), và 17 cặp (29%) là từ ghép thuần Việt hai âm tiết đều có nghĩa riêng (da thịt, vũng lầy, gót chân, giọt nước, trái tim, dòng nước, khung trời, tháng năm, mưa bão, dài lâu, ngời sáng…). So với bản trước (54% / 38% / 8%), tỷ trọng từ láy giảm và tỷ trọng từ ghép thuần Việt tăng rõ — không phải vì Lê Uyên Phương "đổi phong cách" (kho ngữ liệu chỉ đổi thêm 1/21 bài), mà vì mẫu lớn hơn cho PMI đủ cao để lộ diện nhiều từ ghép thuần Việt tần suất thấp hơn (2 lần) mà mẫu 20 bài trước chưa đủ "chạm ngưỡng" xếp hạng. Đây là một minh hoạ khác cho giới hạn đã nêu ở mục 5: tỷ lệ phần trăm ở bảng loại từ nhạy với cỡ mẫu, nên nên đọc là "bức tranh hiện tại của kho dữ liệu đang có", không phải hằng số phong cách tuyệt đối của tác giả.
  • Mảng Hán-Việt (kỷ niệm, tương lai, thời gian, thủy chung, cô liêu, dương trần, ái ân…) cho thấy Lê Uyên Phương pha trộn khá đều giữa khẩu ngữ đời thường và từ vựng "văn chương/trí thức" — phù hợp với hồ sơ một nhạc sĩ xuất thân giáo sư đại học, không thuần là nhạc sĩ viết theo bản năng dân gian.
  • Mục 4 (từ đôi đặc trưng riêng bài) cho thấy phần lớn "từ đôi" nổi bật nhất trong một bài cụ thể không phải từ ghép có sẵn trong từ điển mà là cụm-khóa do chính bài đó tạo ra bằng điệp lại (chiều nào/chiều nầy ở "Còn Nắng Trên Đồi", hãy ngồi/ngồi xuống/xuống đây ở bài cùng tên, xin em/xin anh ở "Lời Gọi Chân Mây") — một kỹ thuật tu từ khác hẳn từ ghép có sẵn, đáng để phân biệt khi phân tích văn bản Lê Uyên Phương: từ ghép có sẵn (kho từ vựng chung của tiếng Việt) và điệp cụm tự tạo (dấu ấn tác giả) là hai hiện tượng khác nhau nhưng dễ gộp nhầm nếu chỉ nhìn số liệu thô.
  • "Tình Khúc Cho Em" đóng góp một quan sát riêng đáng chú ý: cụm "nồng nàn" xuất hiện 4 lần chỉ trong một bài — cao hơn hẳn mức "đặc trưng riêng bài" (mục 4) thông thường (2–4 lần) — vì bài này xây trên cấu trúc đối xứng "Xin cho yêu em nồng nàn" (mở đoạn 2) → "Cho tôi yêu em nồng nàn" (kết bài), mỗi câu tự lặp lại 2 lần liên tiếp (đã ghi trong ký âm). Đây là một trường hợp mà điệp-cụm-tự-tạo (mục 4) và từ-ghép-có-sẵn-trong-từ-điển (mục 2) trùng nhau: "nồng nàn" vừa là một từ láy thật trong tiếng Việt, vừa tình cờ là chính cụm-khóa mà bài này lặp lại nhiều nhất — khác với "hãy ngồi xuống đây" hay "viên đá", vốn là cụm/danh từ riêng của bài chứ không phải từ láy/ghép tự thân.

Lê Uyên Phương — "Từ đôi": Ghi chép quy trình (Process)

1. Yêu cầu gốc

Sau bài thống kê từ đơn-âm-tiết (_LeUyenPhuong_WordUsage_Analysis.md), người yêu cầu muốn một cách nhìn khác: tìm những cặp hai âm tiết trong lời ca chỉ có nghĩa khi đứng chung, vô nghĩa (hoặc nghĩa khác hẳn) khi tách riêng — nêu ví dụ "vòng tay", "dung nhan", "huy hoàng". Yêu cầu gồm 4 file: bài phân tích kết quả + bài ghi lại cách làm (chính file này), mỗi bài có bản .md.html.

2. Vì sao đếm từ đơn không đủ

Bài word-usage trước đếm từng âm tiết rời, nên "vòng" và "tay" chỉ hiện ra như hai dòng riêng trong bảng tần suất — không có gì cho biết chúng thường đứng cạnh nhau. Muốn bắt được hiện tượng "từ đôi", phải đếm cặp âm tiết liền kề theo đúng thứ tự xuất hiện trong câu hát (bigram), rồi đo xem cặp đó có "dính" với nhau hơn mức ngẫu nhiên hay không.

3. Bước 1 — dựng "stream" token có thứ tự cho từng bài

Khác với bài trước (chỉ cần gộp mọi ô "Lời" thành một túi từ không quan tâm thứ tự), bigram bắt buộc phải giữ đúng thứ tự — và phải cẩn thận không nối bigram giữa hai đoạn không liền nhau về nghĩa.

Phát hiện quan trọng khi rà lại cấu trúc bảng: hầu hết file *_KyAm.md có một cột duy nhất tên "Lời" (một dòng lời ca nối tiếp qua các ô), nhưng ít nhất một file (DaXanh_KyAm.md) có cấu trúc khác — ba cột song song "Khổ 1" / "Khổ 2" / "Khổ 3" trong cùng một bảng, tức ba lời khác nhau hát trên cùng một khung nhạc (biến tấu lời qua ba lần lặp giai điệu), không phải một lời nối tiếp. Nếu gộp cả ba cột thành một chuỗi theo thứ tự đọc trái-sang-phải từng hàng, sẽ tạo ra các bigram giả (chữ cuối "Khổ 1" nối bừa với chữ đầu "Khổ 2") — đây chính là loại lỗi mà mục 5 bài Analysis gắn dấu † nghi ngờ.

Cách xử lý: khi đọc header của mỗi bảng, quét cột nào tên "Lời" (gộp chung thành một stream tên loi, nối tiếp qua các bảng kế nhau trong cùng file — vì đó là lời ca thật sự nối trang) và cột nào khớp mẫu "Khổ N" (mỗi số N giữ một stream riêng: kho1, kho2, kho3…). Bigram chỉ được tạo trong nội bộ một stream, không bao giờ bắc cầu giữa kho1kho2, v.v.

4. Bước 2 — tạo bigram

Với mỗi stream (danh sách âm tiết theo đúng thứ tự), bigram là mọi cặp (token[i], token[i+1]). Làm việc này cho cả 21 bài rồi gộp lại toàn kho (số liệu dưới đây là bản đã cập nhật 2026-07-27 — xem mục 7 để biết vì sao khác bản gốc):

  • Tổng âm tiết (unigram): 3.952 (bản gốc, 20 bài, thiếu sót: 3.661)
  • Tổng cặp liền kề (bigram): 3.923 (= tổng âm tiết trừ đi số "đường nối bị cắt" ở ranh giới các stream — mỗi stream có n âm tiết thì chỉ cho n−1 bigram, nên tổng bigram luôn nhỏ hơn tổng unigram một chút)
  • Số cặp khác nhau: 2.878 (bản gốc: 2.712)

5. Bước 3 — công thức PMI, tính tay hai ví dụ mẫu

PMI (pointwise mutual information) đo độ "dính" giữa hai từ so với xác suất chúng đứng cạnh nhau nếu hoàn toàn ngẫu nhiên, độc lập với nhau:

``` PMI(w1, w2) = log2( P(w1, w2) / ( P(w1) × P(w2) ) )

trong đó: P(w1, w2) = số lần cặp (w1,w2) xuất hiện liền kề / tổng số bigram P(w1) = số lần w1 xuất hiện / tổng số unigram P(w2) = số lần w2 xuất hiện / tổng số unigram ```

PMI = 0 nghĩa là hai từ hoàn toàn độc lập (đứng cạnh nhau đúng bằng mức ngẫu nhiên dự đoán); PMI càng dương, hai từ càng "cố ý" đi cùng nhau hơn mức ngẫu nhiên — dấu hiệu của một từ ghép/từ láy thật sự.

Ví dụ 1 — "vòng tay" (đúng ví dụ người yêu cầu nêu; số liệu bản cập nhật 2026-07-27, 21 bài):

  • Cặp "vòng tay" xuất hiện 2 lần; "vòng" xuất hiện tổng cộng 2 lần trong toàn kho (nghĩa là 100% số lần "vòng" xuất hiện, nó luôn đi cùng "tay"); "tay" xuất hiện 24 lần (không đổi so với bản gốc — "tay" không xuất hiện ở bài 21 hay ở phần vừa sửa của "Lời Gọi Chân Mây").
  • P(vòng,tay) = 2/3923 = 0,000510
  • P(vòng) = 2/3952 = 0,000506 · P(tay) = 24/3952 = 0,006073
  • P(vòng)×P(tay) = 0,00000307
  • tỉ số = 0,000510 / 0,00000307 ≈ 165,9
  • PMI = log2(165,9) ≈ 7,37 (bản gốc, 20 bài: 7,26 — xê dịch nhẹ chỉ vì mẫu số toàn kho tăng, tần suất "vòng"/"tay"/"vòng tay" không đổi)

Ví dụ 2 — "dung nhan" và "huy hoàng" (hai ví dụ Hán-Việt người yêu cầu nêu thêm): cả hai cặp chỉ xuất hiện đúng 1 lần trong toàn kho. "dung", "nhan", "huy" vẫn mỗi âm chỉ xuất hiện đúng 1 lần trong 21 bài — nhưng "hoàng" thì không còn đúng như vậy nữa: bài 21 ("Tình Khúc Cho Em") có thêm "bàng hoàng" ("…dù tháng năm buồn vui bàng hoàng…"), nên "hoàng" nay xuất hiện 2 lần toàn kho (1 trong "huy hoàng", 1 trong "bàng hoàng" — hai từ khác nghĩa hoàn toàn). Tính lại:

  • P(dung,nhan) = 1/3923 = 0,000255 · P(dung) = P(nhan) = 1/3952 = 0,000253 → P(dung)×P(nhan) = 0,0000000640 → tỉ số ≈ 3.982 → PMI = log2(3.982) ≈ 11,96 (bản gốc: 11,85)
  • P(huy,hoàng) = 1/3923 = 0,000255 · P(huy) = 1/3952 = 0,000253 · P(hoàng) = 2/3952 = 0,000506 → P(huy)×P(hoàng) = 0,000000128 → tỉ số ≈ 1.991 → PMI = log2(1.991) ≈ 10,96 (bản gốc: 11,85 — giảm rõ hơn "dung nhan" vì mẫu số P(hoàng) tăng gấp đôi trong khi tần suất cặp không đổi)

PMI của hai cặp này vẫn cao hơn hẳn "vòng tay" — nhưng đó là vì mẫu quá nhỏ (chỉ 1 lần quan sát): PMI có xu hướng "thổi phồng" các cặp hiếm, và ví dụ "huy hoàng"/"hoàng" ở trên cho thấy rõ hệ quả thực tế của điều đó — chỉ cần thêm 1 bài vào kho ngữ liệu là đủ để giảm PMI của "huy hoàng" gần 1 điểm, vì giả định ngầm "hoàng chỉ từng đi với huy" không còn đúng 100% nữa. Vì vậy bảng xếp hạng chính ở bài Analysis (mục 2–3) chỉ giữ những cặp xuất hiện ≥2 lần, để tránh nhiễu thống kê; hai ví dụ "dung nhan"/"huy hoàng" được nêu riêng ở mục 5 bài Analysis như minh họa khái niệm, có ghi rõ giới hạn mẫu nhỏ.

6. Bước 4 — lọc nhiễu và loại cụm ngữ pháp

Sau khi xếp hạng theo PMI, nhiều cặp lọt vào top nhưng không phải từ ghép/từ láy thật mà là:

  1. Cụm hô ngữ/ngữ pháp ("em ơi", "một lần", "cho nhau") — có nghĩa rõ khi tách riêng từng chữ, chỉ là cấu trúc câu thông thường, không phải "từ đôi vô nghĩa khi tách rời" theo đúng tinh thần câu hỏi ban đầu.
  2. Điệp khúc/tên bài lặp lại nhiều lần ("hãy ngồi", "ngồi xuống", "xuống đây" — cả ba đều PMI cao vì đúng là tên bài "Hãy Ngồi Xuống Đây" được lặp nguyên cụm trong lời) — đây là hiện tượng tu từ (điệp ngữ) khác với từ ghép có sẵn trong từ điển, nên được tách thành một quan sát riêng ở mục 6 bài Analysis thay vì trộn chung vào bảng "từ đôi".
  3. Mảnh vỡ do ranh giới ô bảng ký âm — như "lai mịt" (nhiều khả năng là phần đuôi "tương lai" + phần đầu "mịt mờ" bị ghép nhầm khi hai cụm nằm sát nhau trong cùng ô, hoặc ở ranh giới hai ô kế tiếp), "ngón suông", "sô có", "đầu phận", "dài phút", "với trái" — các cặp này bị gắn dấu † trong bảng Analysis thay vì đưa vào danh sách "từ đôi" chính thức, vì không đủ tin cậy để khẳng định là từ ghép có thật.

Việc phân loại 3 nhóm trên làm thủ công, dựa vào việc đọc lại từng cặp và kiến thức tiếng Việt của người viết — không phải bước tự động trong script. Đây là giới hạn cần nêu rõ: PMI chỉ cho biết cặp nào "dính" bất thường về mặt thống kê, không tự phân biệt được "từ ghép có sẵn trong từ điển" và "điệp ngữ tự tạo của bài hát" hay "lỗi ranh giới dữ liệu" — ba nguyên nhân này đều tạo ra tín hiệu PMI cao như nhau.

7. Bước 5 — gán nhãn loại từ (từ láy / Hán-Việt / thuần Việt)

Với 58 cặp còn lại sau khi lọc (mục 6, bản 2026-07-27 — bản gốc 20 bài là 39 cặp), gán nhãn loại bằng kiến thức ngôn ngữ thông thường (không phải tra từ điển tự động):

  • Từ láy — hai âm tiết láy âm/láy vần với nhau (bối rối, ngọt ngào, mênh mông, rộn ràng, đắm đuối, lạnh lùng, rã rời, mịt mờ, thì thầm, vời vợi…).
  • Hán-Việt / Hán-Nôm ghép — ít nhất một âm tiết là từ gốc Hán, hiếm hoặc không dùng độc lập trong khẩu ngữ hiện đại (kỷ niệm, tương lai, thời gian, thủy chung, cô liêu, dương trần, ái ân, đơn côi…).
  • Từ ghép thuần Việt — hai âm tiết đều là từ Việt có nghĩa riêng, ghép lại mang nghĩa cụ thể hơn tổng hai nghĩa rời (vũng lầy, đắng cay, da thịt, gót chân, giọt nước, vòng tay, bàn tay…).

Ranh giới giữa ba nhóm không tuyệt đối (nhiều từ là Hán-Nôm hỗn hợp, ví dụ "âm thầm": "âm" gốc Hán, "thầm" thuần Việt) — nhãn trong bảng Analysis chọn theo cách gọi phổ biến nhất trong sách ngữ pháp tiếng Việt, không phải phân loại học thuật chặt chẽ.

8. Bước 6 — "từ đôi đặc trưng theo từng bài"

Tái dùng đúng công thức log-ratio đã dùng cho "từ đặc trưng theo bài" ở bài word-usage trước (_LeUyenPhuong_WordUsage_Analysis.md, mục 5), nhưng áp cho bigram thay vì unigram:

`` score(w1,w2 | bài X) = ( tần suất (w1,w2) trong bài X / tổng bigram bài X ) ÷ ( tần suất (w1,w2) toàn kho / tổng bigram toàn kho ) ``

Chỉ giữ cặp xuất hiện ≥2 lần trong bài đó, lấy 6 cặp điểm cao nhất mỗi bài. Kết quả (mục 4 bài Analysis) cho thấy phần lớn cặp "đặc trưng theo bài" là cụm điệp ngữ tự tạo (chiều nào/chiều nầy, hãy ngồi/ngồi xuống), không phải từ ghép từ điển — đúng như dự đoán ở mục 6 trên.

9. Công cụ & khả năng tái lập

Toàn bộ xử lý bằng script Python (đọc file .md, dùng regex tách bảng markdown và cột "Lời"/"Khổ N", tokenize bằng regex chữ cái + dấu tiếng Việt, đếm bằng collections.Counter, tính PMI bằng math.log2) — chạy ngoài thư mục dự án (trong scratchpad phiên làm việc), chỉ ghi lại kết quả đếm (số liệu bigram/PMI) vào 4 file cuối cùng, không lưu lại nguyên văn lời bài hát nào ở bất kỳ bước trung gian nào. Script có thể chạy lại bất cứ lúc nào nếu kho *_KyAm.md được cập nhật/sửa lỗi, cho kết quả nhất quán vì hoàn toàn dựa trên phép đếm, không có bước ngẫu nhiên.

10. Giới hạn

  • Đây là ký âm đọc kiểu "quick way" (xem _LeUyenPhuong_KyAm_Process.md), lời ca có độ tin cậy cao nhưng không phải bản đối chiếu từng chữ với ấn bản gốc — vài âm tiết hiếm/lạ có thể bị đọc sai (lỗi OCR-bằng-mắt), ảnh hưởng nhẹ đến các cặp tần suất thấp (n=1 hoặc 2).
  • Mẫu 21 bài, ~3.950 âm tiết là nhỏ theo chuẩn thống kê ngôn ngữ học — các con số PMI ở mục 5 bài Analysis nên đọc như "gợi ý đáng chú ý", không phải kết luận chắc chắn về tần suất dùng từ thật của Lê Uyên Phương trên toàn bộ sự nghiệp sáng tác (chỉ 21/hơn 70 bài đã biết).
  • Gán nhãn loại từ (mục 7) làm thủ công, có thể có bất đồng quan điểm ngôn ngữ học ở một vài trường hợp biên (Hán-Nôm hỗn hợp).

11. Đợt cập nhật 2026-07-27 — bổ sung bài 21 + sửa lỗi đếm thiếu

Lý do: người yêu cầu chỉ ra bài Analysis thiếu "Tình Khúc Cho Em" (bài thứ 21), dù file TinhKhucChoEm_KyAm.md đã ký âm xong từ trước (lời ca/hợp âm tin cậy cao, chỉ cao độ mới cần bản KyAmChiTiet riêng). Việc "chỉ cộng thêm 1 bài" tưởng đơn giản nhưng đòi hỏi tính lại từ đầu, vì file gốc không lưu lại bảng đếm unigram/bigram đầy đủ của 20 bài cũ (chỉ lưu kết quả đã lọc, theo đúng nguyên tắc ở mục 9) — không có cách nào "cộng thêm" một bài vào một tổng đã có mà không dựng lại toàn bộ pipeline.

Quy trình lần này:

  1. Viết lại script Python (parse_lup.py + stats_lup.py, chạy trong scratchpad phiên làm việc) dựng stream cho cả 20 file gốc TinhKhucChoEm_KyAm.md, theo đúng quy tắc ở mục 3 (cột "Lời" nối tiếp qua bảng, cột "Khổ N" tách stream riêng), cộng thêm bước làm sạch ô: bỏ mọi văn bản trong ngoặc đơn (...) (chú thích/ghi chú ký âm, ví dụ "(hết bài)", "(lặp câu…)", "(Tacet….)") và mọi dấu **// trước khi tách âm tiết bằng regex chữ-cái-có-dấu.
  2. Kiểm chứng script bằng cách đối chiếu ngược với các con số đã công bố trong bản gốc: số âm tiết/bài ở _LeUyenPhuong_WordUsage_Analysis.md mục 2, và ba số unigram "tay"=24, "vòng"=2, "dung"=1, "nhan"=1, "huy"=1 nêu trong mục 5 (bản trước khi sửa) của chính file này. Script khớp chính xác với 19/20 bài và cả 5 số unigram nêu trên — cho thấy phương pháp tokenize/gộp stream nhất quán với lần làm gốc.
  3. Bài duy nhất không khớp là "Lời Gọi Chân Mây": WordUsage công bố 70 âm tiết, script tính ra 181. Đọc lại file ký âm mới lộ nguyên nhân: bài này có cấu trúc bảng đặc biệt — 2 bảng đầu dùng cột song song "Khổ 1"/"Khổ 2 (cùng nhạc)" (hai lời hát trên cùng một khuông nhạc, giống kiểu Đá Xanh nhưng chỉ 2 khổ thay vì 3), rồi bảng thứ ba hội tụ thành một cột "Lời (chung 2 khổ)" duy nhất cho đoạn kết. Đối chiếu với cách "Đá Xanh" (3 cột Khổ song song) và "Bên Hồ Than Thở" (trộn cột "Lời" + "Khổ 1"/"Khổ 2") đều được cộng đủ tất cả các cột trong bản gốc (khớp số liệu công bố), kết luận hợp lý nhất là: bản đếm gốc của riêng "Lời Gọi Chân Mây" chỉ bắt được cột "Khổ 1" (= đúng 70 âm tiết), bỏ sót cột "Khổ 2" (63 âm tiết) và bảng hội tụ "Lời (chung 2 khổ)" (48 âm tiết) — nhiều khả năng do cấu trúc 3-bảng-2-kiểu-cột của riêng file này (khác mọi file khác trong tập) khiến bước quét cột ở lần làm gốc dừng lại sớm.
  4. Vì đây là lỗi ảnh hưởng đến cả hai file đã công bố trước đó (_LeUyenPhuong_WordUsage_Analysis.md và chính file _LeUyenPhuong_ TuGhep_Analysis.md), và việc sửa sẽ làm thay đổi các số liệu người yêu cầu đã xem qua trước đây, đã hỏi lại người yêu cầu trước khi sửa (thay vì tự ý sửa hoặc tự ý bỏ qua) — người yêu cầu chọn sửa đầy đủ ngay trong đợt này. _LeUyenPhuong_WordUsage_Analysis.md chưa được sửa (nằm ngoài yêu cầu ban đầu — chỉ nêu ở đây để người yêu cầu biết và quyết định sau nếu muốn).
  5. Sau khi có bộ đếm đúng cho 21 bài, dựng lại toàn bộ mục 1–6 của bài Analysis: tổng quan (mục 1), bảng PMI lọc (mục 2 — từ 40 lên 58 cặp, chủ yếu do phần bổ sung của "Lời Gọi Chân Mây" và "Tình Khúc Cho Em" cùng đẩy nhiều cặp từ ghép thật vượt ngưỡng PMI/tần suất), bảng tần suất thô (mục 3), bảng đặc trưng theo bài (mục 4 — thêm dòng "Tình Khúc Cho Em", sửa dòng "Lời Gọi Chân Mây"), và hai ví dụ mẫu (mục 5, xem mục 5 phía trên của chính file Process này).

Bài học quy trình: nguyên tắc "không lưu lại nguyên văn lời bài hát ở bước trung gian" (mục 9) tuy đúng cho việc bảo vệ bản quyền/không chép lại lời ca, nhưng có cái giá là không thể kiểm toán lại hay cộng thêm dữ liệu mới vào một kho thống kê cũ mà không dựng lại toàn bộ từ đầu. Việc script lần này khớp đúng 19/20 bài + 5 con số unigram tham chiếu là may mắn (phương pháp đủ xác định để tái lập), nhưng nếu không có sẵn các "điểm neo" công khai đó (mục 2 WordUsage, mục 5 TuGhep) thì sẽ không có cách nào phát hiện ra lỗi đếm thiếu ở "Lời Gọi Chân Mây".