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.

No comments: