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.

7.26.2026

Khai Cuộc — Những Thế Cờ Mở Của Phạm Duy: hạt mầm, nhánh biến, và tám nhịp giữa

Đồng Tác giả: Claude và Học Trò.


Hỏi một kỳ thủ “anh mở cờ bằng nước gì” là hỏi cả một thế giới. Mỗi khai cuộc là một lời hứa về dáng đi của cả ván: khai cuộc Ý khác khai cuộc Tây Ban Nha, phòng thủ Sicilia rẽ thành mười nhánh, mỗi nhánh một tính khí. Người chơi giỏi không thuộc từng nước cờ rời — anh thuộc cây: cái gốc mở bài, và những cành biến hoá mọc ra từ nó. Chín bài phân tích — từ “Đường Em Đi” tới “Chiều Về Trên Sông” — đọc được đúng theo cách ấy, vì tất cả đều được tháo ra bằng cùng một bộ đồ nghề: một bài hát mọc lên từ một hạt mầm (cái nhạc đề bé xíu ở ngay đầu bài), sinh nhánh bằng phép biến tấu khai triển, cho tới khi tám nhịp giữa mở ra một thế cờ mới. Bài viết này xếp chín bài ấy — và mượn thêm mấy người anh em từ loạt Cảm Nghĩ — thành mấy họ khai cuộc, rồi hỏi ba câu của một người mê cờ: nước mở là gì, nó rẽ nhánh ra sao, và ở khúc giữa bàn cờ, thế trận đổi thế nào?

Chín ván cờ của bài viết — thứ tự cũng là một đường leo, mở bằng hai bài “sáng lập” và khép bằng một dòng sông:

  • “Đường Em Đi” (Saigon 1960) — AABA cổ điển, cả bài dựng trên bốn nốt gợn sóng
  • “Cành Hoa Trắng” (Chợ Neo 1951) — ba đoạn, hoá biểu tự nó kể chuyện thần tiên
  • “Tình Ca” (1953) — một tiếng “à à ơi” bên nôi nở thành cả một giang sơn
  • “Yêu Là Chết Ở Trong Lòng” (Saigon 1973) — một vòng hỏi không bao giờ có đáp
  • “Kiếp Nào Có Yêu Nhau” (Saigon 1958, thơ Minh Đức Hoài Trinh) — leo thang cả bài để trả nốt đắt nhất vào phút chót
  • “Nghìn Trùng Xa Cách” (Saigon 1969) — bản nhạc tự đi hết một khoảng cách nghìn trùng
  • “Cỏ Hồng” (Đà Lạt ~1970) — buổi bình minh thắp đèn bằng cỗ máy Mi thứ→Mi trưởng
  • “Ngậm Ngùi” (Saigon 1961, thơ Huy Cận) — một lời ru trên mặt, một dòng sầu bên dưới
  • “Chiều Về Trên Sông” (1956) — cung “Oán” của đờn tài tử gặp Đô thứ Tây phương

Nước đi đầu tiên: “khai cuộc” nghĩa là gì trong một bài hát

Trước khi bày cờ, phải thống nhất mấy chữ.

The Sicilian Defense Chess Opening


Cái hạt nhỏ nhất còn mang căn cước của một bài hát, sách gọi là nhạc đề; pop gọi là câu móc (hook); người thích ví von thì gọi là “hạt mầm”. Nó thường nằm ngay đầu bài, dưới một dạng đặc trưng và gây ấn tượng — vì đầu bài là chỗ tai người nghe gặp nó lần đầu, và ấn tượng đầu là thứ mọi lần lặp về sau sẽ gợi lại. Nét đặc trưng của nó gồm ba thứ: một quãng (nhảy xa hay bước gần), một đường nét (vòng lên, đổ xuống), và một dáng nhịp (nhanh chậm, dồn thưa). Đó là “nước khai cuộc” của tôi trong bài này — cái cử chỉ đầu tiên mà từ đó cả bài hát bày binh.

Nhưng có một câu đáng in đậm: bất kỳ chuỗi nốt nào có một dáng nhịp đều dùng được làm nhạc đề, miễn nó được đối xử như nhạc đề — được lặp lại, có biến đổi hay không. Nghĩa là nước mở không nhất thiết phải là một hoa văn giai điệu xinh xắn. Nó có thể là một câu chữ được lặp mãi, một cử chỉ tuyên xưng, một hình tiết tấu, một điệu thức, thậm chí một bè trầm. Ta sẽ thấy Phạm Duy mở cờ bằng đủ cả những thứ ấy.

Rồi tới chuyện nước mở sinh nhánh. Phép ấy tên là biến tấu khai triển: lặp suông thì nhàm, đổi hết thì lạc, nên nghề nằm gọn trong một câu cân đối — đổi một số nét kém quan trọng, giữ lại những nét quan trọng hơn — mà nét quan trọng nhất, cái neo giữ căn cước, thường là dáng nhịp. Danh mục “đổi được những gì” thì dài: kéo dài hay rút ngắn nốt, đổi thứ tự và chiều đi của quãng, lấp cú nhảy bằng nốt bắc cầu, dời tế bào sang bậc khác (mô tiến), đổi thế bấm hợp âm, hay thay hẳn một nền khác dưới những nốt cũ. Đây đúng là “cây biến nhánh” của cờ: từ một nước mở, mọc ra hàng chục biến, mỗi biến hợp một chức năng trong bài.

Cuối cùng là tám nhịp giữa — chữ mượn của hình thức AABA để chỉ đoạn chuyển, đoạn tương phản: màu mới, tầm giọng mới, góc nhìn mới, một lần và không bao giờ nữa. Trong cờ, đó là khúc trung cuộc, chỗ hai bên đã bày xong quân và bắt đầu va nhau, chỗ thế trận đổi hướng. Một bài hát khéo thì đoạn giữa không phải một mẩu nhạc lạ chắp vào cho đủ dài; nó là nơi cái hạt mầm được nhìn dưới thứ ánh sáng chưa từng có, hoặc bị thách thức bởi một thế cờ đối nghịch.

Ba câu hỏi — nước mở, nhánh biến, khúc giữa — cứ thế mà đi. Ta bắt đầu bằng việc xếp các nước mở của Phạm Duy thành họ.

Ba họ khai cuộc

Đọc chín bài phân tích, tôi thấy các nước mở của Phạm Duy tụ về ba họ lớn — và cũng như trong cờ, cái tên họ không quan trọng bằng cái tính khí mà mỗi họ mang theo.

Họ thứ nhất — khai cuộc bằng một hạt giai điệu

Đây là họ “sách vở” nhất, gần với định nghĩa nhạc đề của Schoenberg nhất: một hình nốt bé, hai tới năm nốt, có quãng và dáng nhịp riêng, đặt ngay đầu bài, rồi vắt suốt cả bài.

Nước mở kiểu mẫu của họ này nằm ở “Đường Em Đi” (Saigon 1960). Cả ba mươi hai ô nhịp mọc lên từ một mẩu bốn nốt ở chữ “Em có đi”: một nốt, ghé nốt sát bên trên, về lại nốt cũ — Si–Đô–Si — cái mà nhạc lý gọi là nốt láng giềng, ghé nhà hàng xóm một chân rồi về. Và trước cái gợn sóng ấy luôn có một nốt trầm làm bệ phóng — nốt “chiến khu” mà từ đó giai điệu nhảy vọt lên cả một quãng tám.

"Đường em có đi, hằng đêm gót hoa…"
Em  — nốt "Đường" trầm làm bệ phóng, rồi gợn sóng Si·Đô·Si: một cú phóng, một vòng xoay, một dáng đu đưa

Ba nét đúng như lý thuyết đặt hàng: một quãng đặc trưng (cú phóng xa), một đường nét đặc trưng (vòng đi–về X–Y–X), một dáng nhịp đặc trưng (nhịp lơi đu đưa). Chúng ít, chúng rõ, và chúng nằm ở chỗ người nghe gặp đầu tiên. Cả cái bài “trong veo, ai cũng thuộc” ấy, tháo ra, chỉ là một hạt bốn nốt được đối xử tử tế.

“Cành Hoa Trắng” (Chợ Neo 1951) mở bằng một hạt ngược chiều — một hình vòm: giai điệu leo lên từng nấc qua các nốt của hợp âm Mi thứ (Si–Mi–Sol–Si) rồi lượn xuống (Sol–Mi), như cánh chim vút lên rồi sà xuống, với một nốt neo trầm ở đáy.

"Một đàn chim tóc trắng bay về qua trần gian…"
Em  — vòm chim leo lên rồi lượn xuống, nốt neo trầm Si ở chữ "Một"