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>

Wow, what a lot of code! versus 88 for the old script.
ReplyDeleteThat's one of the first things I noticed. The old script is 78 lines, this is 433 lines. And yet the new script runs six times as fast. I'm not exaggerating, try it for yourself. I'm especially interested in hearing from anyone who has more posts than me (currently 6350).
Delete