Eleven years ago I released an app to list blog posts in alphabetical order.
Since then it's been working fine, apart from one minor bug that most of you
probably never noticed. On Friday I asked ChatGPT to write me a new app. The
result is amazing. The old app needed 36 seconds to list my 6348 posts. The
new app only needs 6 seconds.
This is the new code. Copy it into a blogspot page. No configuration is
necessary. If you use it, please leave a comment telling me your blog
address.
These are early days. Consider it a beta test.
This is a link to the old code,
in case you want to go back. If the test goes well, I'll delete the old code
from my blog on 1st September.
<!-- Blogger Alphabetical Post Index - Version 2.14
Paste this into the HTML view of a Blogger Page.
Version 2.14:
- Based on the working Version 2.11 retrieval method.
- Shows publication date before each post title.
- Sorts alphabetically by title.
- If titles are identical, older posts come first.
- Uses Blogger's lightweight JSONP summary feed.
- Requests 100 posts at a time using fixed start-index
positions.
- Loads 5 batches in parallel for speed.
- No API key, blog ID or blog URL is required.
-->
<div id="alphabetical-post-index">
<p id="api-status">Loading posts... 0 found</p>
<ul id="api-post-list"></ul>
</div>
<style>
#alphabetical-post-index {
width: 100%;
}
#api-status {
margin: 0 0 1em 0;
}
#api-post-list {
margin: 0;
padding-left: 1.5em;
}
#api-post-list li {
margin: 0.2em 0;
}
#api-post-list .post-date {
display: inline-block;
margin-right: 0.8em;
white-space: nowrap;
color: #777;
}
#api-post-list a {
text-decoration: none;
}
#api-post-list a:hover {
text-decoration: underline;
}
</style>
<script>
(function () {
"use strict";
const BATCH_SIZE = 100;
const PARALLEL_REQUESTS = 5;
const RETRIES = 2;
const status = document.getElementById("api-status");
const list = document.getElementById("api-post-list");
const postsByUrl = new Map();
let callbackCounter = 0;
function blogRoot() {
return window.location.protocol + "//" + window.location.host
+ "/";
}
function updateProgress() {
status.textContent =
"Loading posts... " +
postsByUrl.size.toLocaleString("en-GB") +
" found";
}
function canonicaliseUrl(url) {
try {
const u = new URL(url, blogRoot());
u.hash = "";
if (u.host === window.location.host) {
u.protocol = window.location.protocol;
}
return u.href;
} catch (error) {
return url;
}
}
function formatDate(dateString) {
if (!dateString) {
return "";
}
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return year + "-" + month + "-" + day;
}
function jsonp(url) {
return new Promise(function (resolve, reject) {
callbackCounter++;
const callbackName =
"__alphabeticalIndexCallback" + callbackCounter;
const script = document.createElement("script");
let finished = false;
const timer = setTimeout(function () {
if (finished) {
return;
}
finished = true;
cleanup();
reject(new Error("Blogger feed request timed
out."));
}, 15000);
function cleanup() {
clearTimeout(timer);
try {
delete window[callbackName];
} catch (error) {
window[callbackName] = undefined;
}
if (script.parentNode) {
script.parentNode.removeChild(script);
}
}
window[callbackName] = function (data) {
if (finished) {
return;
}
finished = true;
cleanup();
resolve(data);
};
script.onerror = function () {
if (finished) {
return;
}
finished = true;
cleanup();
reject(new Error("Blogger feed request
failed."));
};
script.src =
url +
(url.indexOf("?") >= 0 ? "&" : "?") +
"callback=" +
encodeURIComponent(callbackName);
script.async = true;
document.body.appendChild(script);
});
}
async function jsonpWithRetry(url) {
let lastError = null;
for (let attempt = 0; attempt <= RETRIES; attempt++) {
try {
return await jsonp(url);
} catch (error) {
lastError = error;
if (attempt < RETRIES) {
await new Promise(function (resolve) {
setTimeout(resolve, 300 * (attempt
+ 1));
});
}
}
}
throw lastError;
}
function feedUrl(startIndex) {
return (
blogRoot() +
"feeds/posts/summary" +
"?alt=json" +
"&start-index=" + startIndex +
"&max-results=" + BATCH_SIZE
);
}
function getAlternateLink(entry) {
if (!entry.link) {
return "";
}
for (const link of entry.link) {
if (link.rel === "alternate") {
return canonicaliseUrl(link.href);
}
}
return "";
}
function addEntries(data) {
if (!data || !data.feed) {
throw new Error("Blogger returned an invalid feed.");
}
const entries = data.feed.entry || [];
for (const entry of entries) {
const title =
entry.title &&
typeof entry.title.$t === "string"
? entry.title.$t.trim()
: "";
const url = getAlternateLink(entry);
const published =
entry.published &&
entry.published.$t
? entry.published.$t
: "";
if (!title || !url) {
continue;
}
if (!postsByUrl.has(url)) {
postsByUrl.set(url, {
title: title,
url: url,
published: published
});
}
}
updateProgress();
return entries.length;
}
function getTotalResults(data) {
try {
return Number(
data.feed["openSearch$totalResults"].$t
) || 0;
} catch (error) {
return 0;
}
}
async function loadBatch(startIndex) {
const data =
await jsonpWithRetry(feedUrl(startIndex));
addEntries(data);
return data;
}
async function worker(queue, failures) {
while (queue.length) {
const startIndex = queue.shift();
try {
await loadBatch(startIndex);
} catch (error) {
console.warn(
"Feed batch failed at start-index",
startIndex,
error
);
failures.push(startIndex);
}
}
}
function comparePosts(a, b) {
const result = a.title.localeCompare(
b.title,
"en",
{
sensitivity: "base",
numeric: true
}
);
if (result !== 0) {
return result;
}
return new Date(a.published) - new Date(b.published);
}
function renderPosts() {
const posts =
Array.from(postsByUrl.values());
posts.sort(comparePosts);
const fragment =
document.createDocumentFragment();
for (const post of posts) {
const li =
document.createElement("li");
const date =
document.createElement("span");
date.className = "post-date";
date.textContent =
formatDate(post.published);
const link =
document.createElement("a");
link.href = post.url;
link.textContent = post.title;
li.appendChild(date);
li.appendChild(link);
fragment.appendChild(li);
}
list.replaceChildren(fragment);
status.textContent =
posts.length.toLocaleString("en-GB") +
" published posts";
}
async function start() {
const firstData =
await loadBatch(1);
const totalResults =
getTotalResults(firstData);
if (!totalResults) {
throw new Error(
"Blogger did not report the total number of
posts."
);
}
const queue = [];
for (
let startIndex = 1 + BATCH_SIZE;
startIndex <= totalResults;
startIndex += BATCH_SIZE
) {
queue.push(startIndex);
}
const failures = [];
const workers = [];
const workerCount = Math.min(
PARALLEL_REQUESTS,
queue.length
);
for (let i = 0; i < workerCount; i++) {
workers.push(
worker(queue, failures)
);
}
await Promise.all(workers);
if (failures.length > 0) {
const retryList = failures.slice();
failures.length = 0;
for (const startIndex of retryList) {
try {
await loadBatch(startIndex);
} catch (error) {
failures.push(startIndex);
}
}
}
renderPosts();
if (failures.length > 0) {
status.textContent =
postsByUrl.size.toLocaleString("en-GB") +
" posts found; " +
failures.length.toLocaleString("en-GB") +
" feed batch" +
(failures.length === 1 ? "" : "es") +
" couldn't be read.";
}
}
start().catch(function (error) {
console.error(
"Alphabetical Post Index:",
error
);
status.textContent =
"Loading stopped after " +
postsByUrl.size.toLocaleString("en-GB") +
" posts.";
status.style.fontWeight = "bold";
});
})();
</script>




















