Wednesday, 3 September 2025

Alphabetical List Test Version


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


2 comments:

  1. Thanks so much !
    764 posts found at https://dt-hs.blogspot.com/2026/09/sitemap-generator-3-collecting-more.html

    Would love to know how I can fix the problems with my blog you have detected....

    e.g. any suggestions re. how I can find the missing post without a title ?

    Thanks so much for spending time on this issue.

    ReplyDelete
    Replies
    1. Hi Stephen. The problems are with Blogger, not my alphabetical app itself. Some of your posts have a lot of metadata, which is a combination of the post size, the number of images, the length of the title and some other details. I had to rewrite the app (with the help of AI) to be error-tolerant and reread these posts.

      I can only say that the post without the title was the 548th post that you wrote. So it must be somewhere near the top third in your recent posts.

      Delete

Tick the box "Notify me" to receive notification of replies.