Tuesday, 1 September 2026

Alphabetical List of Posts




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.



<!-- 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>