After having passed its beta test, this is the new version of my code for
creating an alphabetical list of blog posts. It's not just a modified version,
it's completely new code written by AI. I'm impressed. The old app needed
36 seconds to list my 6348 posts. The new app only needs 6 seconds. I've now
removed the page with the old code that I posted in 2015. I strongly recommend
that everyone should update.
Unlike the old version, no configuration is required. Copy the code into a
stand-alone blogspot page, and away you go.
The code is free to copy and use. All I ask is that you leave a comment
telling me the address of your blog and how many posts you currently have. I'm
particularly interested in hearing from bloggers who have more posts than me.
Addendum on 3. September 2026
It's been pointed out to me that if a post has no title it's missing from the
list. This is deliberate. The code is for sorting posts, and if a post has no
title there's nothing to sort. Please remember to give all of your posts titles!
<!-- 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).
DeleteWorks !!! Great step forward.....
ReplyDeleteBUT it permanently changed my theme ??? See: dt-hs.blogspot.com. the colour has bled across the posts, and the Latest Posts gadget does not work anymore ????
Wierd that it can change the theme permanently ???
Also a requested enhancement: How can I remove the date from the left of the post name ??
Not to be ungratefull. I have been waiting fort his for YEARS !! Thanks Thanks Thanks
I don't understand your problem. Can you give me a direct link to your page for the alphabetical list, please?
DeleteThe date used to be on the right. I never liked it, because it made the list look messy. By having the date on the left it looks tidier. I didn't actually write the code myself, but I can look into a way of removing the date altogether.
Oh wait, I found it: SiteMap Test Sep 2026. But I can't see what the problem is.
DeleteWhoops.
ReplyDeleteFound another issue as seen on https://dt-hs.blogspot.com/2026/09/sitemap-test-sep-2026.html.
The new code generates a list of 476 published posts. But the blog archive gadget shows 764.......
I've created a new version. It finds 763 out of 764 posts. The missing post is a post without a title. You can find it at
Deletehttps://dansator.blogspot.com/2025/09/alphabetical-list-test-version.html
Try this version for a few weeks. If it works I'll consider promoting it to the main version. I'm hesitating because there are problems in your blog that nobody else has.
Thanks so. much Mike.
DeleteI pasted the code into Claude adn asked it to remove dates. Works fine.
Completely happy now thanks to you.
I intend to use the list generator about weekly and then copy the result into a static page for faster loading.
My latest list generator is at https://dt-hs.blogspot.com/2026/09/sitemap-generator-4-claude-no-date.html
Do you want that code ?