<!-- Blogger Alphabetical Post Index - Version 2.24
Final version with adaptive Blogger feed recovery.
-->
<div id="alphabetical-post-index">
<p id="api-status">Loading posts...</p>
<div id="api-results"></div>
</div>
<style>
#alphabetical-post-index { width: 100%; }
#api-status { margin: 0 0 1em 0; }
.post-row {
margin: 0 0 0.45em 0;
line-height: 1.4;
}
.post-date {
color: #777;
margin-right: 0.9em;
}
.post-link {
text-decoration: none;
}
.post-link:hover {
text-decoration: underline;
}
</style>
<script>
(function () {
"use strict";
const BLOG_ROOT = window.location.origin + "/";
const INITIAL_BATCH_SIZE = 100;
const RECOVERY_SIZES = [50, 25, 10, 5, 1];
const PARALLEL_REQUESTS = 5;
const RETRIES = 2;
const status = document.getElementById("api-status");
const results = document.getElementById("api-results");
let callbackCounter = 0;
let posts = [];
function jsonp(url) {
return new Promise(function (resolve, reject) {
callbackCounter++;
const callbackName =
"__bloggerIndexCallback" + callbackCounter;
const script = document.createElement("script");
let finished = false;
const timer = setTimeout(function () {
if (finished) return;
finished = true;
cleanup();
reject(new Error("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("Request failed"));
};
script.src =
url +
"&callback=" +
encodeURIComponent(callbackName);
script.async = true;
document.body.appendChild(script);
});
}
async function request(start, count) {
let lastError = "";
for (let attempt = 0; attempt <= RETRIES; attempt++) {
try {
const data =
await jsonp(
BLOG_ROOT +
"feeds/posts/summary?alt=json" +
"&start-index=" + start +
"&max-results=" + count
);
const entries =
data &&
data.feed &&
data.feed.entry
? data.feed.entry
: [];
let total = null;
if (
data &&
data.feed &&
data.feed.openSearch$totalResults &&
data.feed.openSearch$totalResults.$t !== undefined
) {
total = Number(
data.feed.openSearch$totalResults.$t
);
}
return {
start: start,
requested: count,
returned: entries.length,
entries: entries,
total: total
};
} catch (error) {
lastError = error.message;
if (attempt < RETRIES) {
await new Promise(function (resolve) {
setTimeout(
resolve,
300 * (attempt + 1)
);
});
}
}
}
return {
start: start,
requested: count,
returned: -1,
entries: [],
total: null,
error: lastError
};
}
function validEntry(entry) {
if (!entry) return false;
const title =
entry.title &&
entry.title.$t
? entry.title.$t.trim()
: "";
let url = "";
if (entry.link) {
for (let i = 0; i < entry.link.length; i++) {
if (
entry.link[i].rel === "alternate" &&
entry.link[i].href
) {
url = entry.link[i].href;
break;
}
}
}
return !!title && !!url;
}
function makePost(entry) {
let title = entry.title.$t.trim();
let url = "";
for (let i = 0; i < entry.link.length; i++) {
if (
entry.link[i].rel === "alternate" &&
entry.link[i].href
) {
url = entry.link[i].href;
break;
}
}
let date = "";
if (
entry.published &&
entry.published.$t
) {
const d = new Date(entry.published.$t);
if (!isNaN(d.getTime())) {
date =
String(d.getDate()).padStart(2, "0") +
"/" +
String(d.getMonth() + 1).padStart(2, "0") +
"/" +
d.getFullYear();
}
}
return {
title: title,
url: url,
date: date,
published: entry.published &&
entry.published.$t
? entry.published.$t
: ""
};
}
function addEntries(entries) {
for (const entry of entries) {
if (!validEntry(entry)) continue;
const post = makePost(entry);
if (
!posts.some(function (existing) {
return existing.url === post.url;
})
) {
posts.push(post);
}
}
updateStatus();
}
function updateStatus() {
status.textContent =
"Loading posts... " +
posts.length +
" found";
}
function sortPosts() {
posts.sort(function (a, b) {
const titleCompare =
a.title.localeCompare(
b.title,
"en",
{
sensitivity: "base",
numeric: true
}
);
if (titleCompare !== 0) {
return titleCompare;
}
return a.published.localeCompare(
b.published
);
});
}
function displayPosts() {
results.innerHTML = "";
const fragment =
document.createDocumentFragment();
for (const post of posts) {
const row =
document.createElement("div");
row.className = "post-row";
const date =
document.createElement("span");
date.className = "post-date";
date.textContent =
post.date;
const link =
document.createElement("a");
link.className = "post-link";
link.href = post.url;
link.textContent = post.title;
link.target = "_blank";
link.rel = "noopener";
row.appendChild(date);
row.appendChild(link);
fragment.appendChild(row);
}
results.appendChild(fragment);
}
async function recoverRange(
start,
end,
failedResult
) {
const expected =
end - start + 1;
for (const size of RECOVERY_SIZES) {
let position = start;
let recoveredEntries = [];
let complete = true;
while (position <= end) {
const count =
Math.min(
size,
end - position + 1
);
const result =
await request(
position,
count
);
if (
result.returned < 0 ||
result.returned !== count
) {
complete = false;
break;
}
recoveredEntries =
recoveredEntries.concat(
result.entries
);
position += count;
}
if (complete) {
addEntries(recoveredEntries);
return true;
}
}
// Last resort: try every position individually.
// This should only be reached if all larger recovery
// sizes fail.
let position = start;
let recoveredEntries = [];
while (position <= end) {
const result =
await request(position, 1);
if (
result.returned === 1
) {
recoveredEntries.push(
result.entries[0]
);
}
position++;
}
addEntries(recoveredEntries);
return recoveredEntries.length === expected;
}
async function processRange(
start,
end
) {
const expected =
end - start + 1;
const initial =
await request(
start,
expected
);
if (
initial.returned === expected
) {
addEntries(initial.entries);
return;
}
await recoverRange(
start,
end,
initial
);
}
async function start() {
try {
// First request establishes Blogger's reported
// total number of published posts.
const first =
await request(
1,
INITIAL_BATCH_SIZE
);
if (first.total === null) {
throw new Error(
"Blogger did not report a total post count."
);
}
const total =
first.total;
if (first.returned === 0) {
status.textContent =
"No posts found.";
return;
}
// Process the first range using the response
// already obtained, avoiding a duplicate request.
if (
first.returned ===
Math.min(
INITIAL_BATCH_SIZE,
total
)
) {
addEntries(first.entries);
} else {
await recoverRange(
1,
Math.min(
INITIAL_BATCH_SIZE,
total
),
first
);
}
// Build all remaining fixed positional ranges.
const ranges = [];
for (
let start = INITIAL_BATCH_SIZE + 1;
start <= total;
start += INITIAL_BATCH_SIZE
) {
ranges.push({
start: start,
end: Math.min(
start +
INITIAL_BATCH_SIZE -
1,
total
)
});
}
// Process ranges in small parallel groups.
// Each range remains positional, so a short Blogger
// response can never shift the following range.
for (
let i = 0;
i < ranges.length;
i += PARALLEL_REQUESTS
) {
const group =
ranges.slice(
i,
i + PARALLEL_REQUESTS
);
await Promise.all(
group.map(function (range) {
return processRange(
range.start,
range.end
);
})
);
}
sortPosts();
displayPosts();
status.textContent =
posts.length +
" posts found";
} catch (error) {
status.textContent =
"Unable to load posts.";
results.textContent =
error.message;
}
}
start();
})();
</script>