If you want to improve the user experience of your website while also boosting your search engine optimization (SEO), adding an HTML sitemap is a great place to start. An HTML sitemap is a map of your website that lists all the pages and content in an organized and easy-to-read format.

Not only does an HTML sitemap make it easier for visitors to find the information they're looking for, it also helps search engines like Google crawl and index your site more efficiently. By providing a clear and organized structure of your website, an HTML sitemap helps search engines understand the content and purpose of each page. This can lead to improved rankings and increased visibility in search engine results pages (SERPs).

While XML sitemaps are mainly intended for search engines, HTML sitemaps are designed for people. They provide a user-friendly way to navigate your site and discover new content. And with the added bonus of improved SEO, there's really no reason not to add an HTML sitemap to your website.

I've written a code that generates an HTML sitemap for your Blogger, with the category filter option. All you need to do is create a new page and paste in the code. Here's a detailed guide that will walk you through the process step by step.

Steps to add an HTML sitemap to your Blogger

Here are the easy steps to add a dynamic HTML sitemap to your Blogger:

  • First, copy the code provided below or simply click here to copy the code
  • Log in to your Blogger account.
  • From the menu, select Pages and then click the "+ New Page" button to create a new page.
  • Give your new page a title, like "Sitemap."
  • Make sure you're in "HTML view" mode, which you can switch to by clicking the first button at the top of the editor.
  • Paste the code you copied earlier into the editor and hit the publish button.
  • Now, you can add the URL of this page like /p/sitemap.html to your main menu or your footer.

Your new page will retrieve all your posts and generate a sitemap automatically. That means you won't have to update it manually.

HTML Sitemap without category filter

If you want to show a sitemap without category filtering, simply use the initWithoutFilter function instead of initWithFilter:

...
  function onSitemapLoad(json) {
    initWithoutFilter();
    ...

HTML Sitemap from specific categories

If you want to display only posts from specific categories, you can easily modify the code to do so. For instance, if you want to show the posts of the category Programming then replace this part of code: <script id="sitemapXML" ... </script> with this one:

<script id="sitemapXML" src="/feeds/posts/summary/-/Programming?callback=onSitemapLoad&alt=json-in-script&max-results=9999" async></script>

Or if you want to show posts that are labeled as Programming and JavaScript then replace with this one:

<script id="sitemapXML" src="/feeds/posts/summary/-/Programming/JavaScript?callback=onSitemapLoad&alt=json-in-script&max-results=9999" async></script>

So the pattern looks like this:
<script...summary/-/First Category/Second Category/...?callback...

If the category name contains from multiple words the spaces between them should be replaced with %20.

Result

Keep in mind that you might need to edit the CSS styling to suit your needs. Also, this code uses plain JavaScript (Vanilla JS) so there's no need for extra libraries.

One thing to note is that it might not work properly in preview mode, but it should work just fine once published.

After completing the steps and publishing your new page, the end result should look like this:

HTML Sitemap result preview

Source code

The complete source code for adding a dynamic HTML sitemap to your Blogger page:

<div id="sitemapContainer">
    <div id="sitemapFilter"></div>
    <div id="sitemapPosts">...</div>
  </div>
  
  <script id="sitemapXML" src="/feeds/posts/summary?callback=onSitemapLoad&alt=json-in-script&max-results=9999" async></script>
  
  <!-- Get posts with LABEL1 and LABEL2 -->
  <!--script id="sitemapXML" src="/feeds/posts/summary/-/LABEL1/LABEL2?callback=onSitemapLoad&alt=json-in-script&max-results=9999" async></script-->
  
  <script>/*<![CDATA[*/
    const ALL = '__all__';
    const getPostsCont = function() { return document.getElementById('sitemapPosts'); }
    const getFilterCont = function() { return document.getElementById('sitemapFilter'); }
    
    function onSitemapLoad(json) {
      
      initWithFilter();
      // initWithoutFilter();
      
      // --------------------------------------
      function initWithFilter() { init(true) }
      function initWithoutFilter() { init(false) }
      
      function init(withFilter) {
        const {posts, postCountByCat} = getPosts(json);
        renderPosts(posts);
        if (!withFilter) return;
        
        const cats = getCats(json);
        const filterObj = renderFilter(cats, postCountByCat);
        filterObj.onFilterChange = function(activeCats) {
          const {posts, postCountByCat} = getPosts(json, activeCats);
          renderPosts(posts);
          filterObj.updatePostCount(postCountByCat);
        }
      }
      
      // --------------------------------------
      function getCats(json) {
        if (!json || !json.feed || !json.feed.category) return [];
        const res = [ALL]
        const arr = json.feed.category;
        for (let i = 0; i < arr.length; i++) {
          if (!arr[i] || !arr[i].term) continue;
          res.push(arr[i].term);
        }
        return res;
      }
  
      function getPosts(json, activeCats) {
        const resObj = {posts: [], postCountByCat: {}}
        if ((activeCats && activeCats.size === 0) || !json || !json.feed || !json.feed.entry) return resObj;
        const arr = json.feed.entry;
        for (let i = 0; i < arr.length; i++) {
          const item = arr[i] || {};
          const cats = (item.category || []).map(function (val) { return val.term; });
          
          // filter
          if (activeCats && !cats.some( function (cat) { return activeCats.has(cat); })) continue;
          
          const obj = {
            cats: cats,
            thumb: item['media$thumbnail'].url,
            summary: (item.summary || {}).$t,
            title: (item.title || {}).$t,
            link: (item.link || []).find(function(val) { return val.rel === 'alternate' }),
          };
          obj.cats.forEach(function (catName) {
            resObj.postCountByCat[catName] = (resObj.postCountByCat[catName] || 0) + 1;
          });
          if (obj.title) resObj.posts.push(obj);
        }
        resObj.postCountByCat[ALL] = resObj.posts.length;
        return resObj;
      }
      
      
      // --------------------------------------
      function renderFilter(cats, postCountByCat) {
        const root = document.createElement('fieldset');
        root.classList.add('filterInner');
        for (let i = 0; i < cats.length; i++) {
          if (!cats[i]) continue;
          const title = cats[i];
          const id = 'sitemapfilter' + i;
          
          const cont = document.createElement('span');
          cont.classList.add('label');
          
          const input = document.createElement('input');
          input.setAttribute('type', 'checkbox');
          input.setAttribute('id', id);
          input.setAttribute('name', title);
          input.toggleAttribute('checked');
          cont.appendChild(input);
          
          const label = document.createElement('label');
          label.setAttribute('for', id);
          cont.appendChild(label);
          
          const titleEl = document.createElement('span');
          titleEl.classList.add('labelTitle');
          titleEl.appendChild(document.createTextNode(title === ALL ? 'All' : title));
          label.appendChild(titleEl);
          
          const countEl = document.createElement('span');
          countEl.classList.add('postCount');
          countEl.appendChild(document.createTextNode(postCountByCat[title] || 0));
          label.appendChild(countEl);
          
          root.appendChild(cont);
        }
        
        getFilterCont().replaceChildren(root);
        
        const filterCatElems = getFilterCont().querySelectorAll('input');
        const filterCatElemAll = filterCatElems[0];
        
        const res = {
          updatePostCount: function (postCountByCat) {
            for (const catElem of filterCatElems) {
              catElem.parentElement.querySelector('.postCount').textContent = postCountByCat[catElem.name] || 0;
            }
          },
          onFilterChange: function (activeCats) {},
        };
        
        for (const catElem of filterCatElems) {
          catElem.addEventListener('click', onFilterChange);
        }
        
        function onFilterChange(e) {
          if (e.target === filterCatElemAll) {
            for (const catElem of filterCatElems) {
              catElem.checked = filterCatElemAll.checked;
            }
          }
  
          const filterCats = new Set()
          for (const catElem of filterCatElems) {
            if (catElem.checked && catElem !== filterCatElemAll) {
              filterCats.add(catElem.name);
            }
          }
  
          if (filterCats.size === filterCatElems.length - 1) {
            filterCatElemAll.checked = true;
          } else if (filterCats.size === 0) {
            filterCatElemAll.checked = false;
          }
          
          res.onFilterChange(filterCats);
        }
        
        return res;
      }
  
      function renderPosts(posts) {
        const container = getPostsCont();
        const root = document.createElement('ul');
        for (let i = 0; i < posts.length; i++) {
          const cont = document.createElement('li');
          
          const a = document.createElement('a');
          a.appendChild(document.createTextNode(posts[i].title));
          a.setAttribute('href', posts[i].link.href);
          a.setAttribute('title', posts[i].title);
          cont.appendChild(a);
          
          const cats = document.createElement('div');
          cats.setAttribute('id', 'categories');
          posts[i].cats.forEach(function (val) {
            const label = document.createElement('span');
            label.classList.add('cat', 'label');
            label.appendChild(document.createTextNode(val));
            cats.appendChild(label);
          })
          cont.appendChild(cats);
          
          root.appendChild(cont);
        }
        container.replaceChildren(root);
      }
    }
  /*]]>*/</script>
  
  <style>
    #sitemapContainer .filterInner {
      padding-bottom: 16px;
    }
    #sitemapContainer .filterInner .label > * {
      margin-right: 3px;
      margin-left: 3px;
    }
    #sitemapContainer .filterInner .label {
      margin-right: 7px;
    }
    #sitemapContainer .filterInner .postCount {
      font-size: 0.67em;
      background-color: #fff;
      color: black;
      padding: 2px 8px;
      border-radius: 10px;
    }
    .dark #sitemapContainer .filterInner .postCount {
      background-color: #999;
    }
    #sitemapContainer .filterInner .labelTitle {
      font-size: 0.9em;
      margin-right: 6px;
    }
    #sitemapContainer .cat {
      font-size: 0.6em;
      line-height: 1.4em;
    }
    #sitemapContainer .label {
      display: inline-block;
      padding: 4px 10px;
      margin-right: 4px;
      margin-bottom: 6px;
      background-color: #efefef;
      color: #333;
      border-radius: 12px;
    }
    .dark #sitemapContainer .label {
      background-color: #3A3A3A;
      color: #ccc;
    }
    #sitemapContainer .filterInner input[type="checkbox"] {
      appearance: auto;
    }
    #sitemapContainer .filterInner input[type="checkbox"],
    #sitemapContainer .filterInner input[type="checkbox"]+label{
      cursor: pointer;
      -webkit-touch-callout: none; /* iOS Safari */
      -webkit-user-select: none; /* Safari */
       -khtml-user-select: none; /* Konqueror HTML */
         -moz-user-select: none; /* Old versions of Firefox */
          -ms-user-select: none; /* Internet Explorer/Edge */
              user-select: none;
    }
  </style>