Dynamic site map with live search And visual interface (OUTDATED)
This post pertains to a dynamic sitemap interface. It is currently under re-consideration and further development and has as such changed considerably. The post below is for archival purposes and will be replaced when and if I ever decide on the type of statistical display to wrap my sitemap into.
This is a chunk of code that creates a dynamically updating site map, so that you don’t have to add pages to it manually. Note that squarespace automatically keeps a .xml version of a site map, the one that search engines use. You can view yours if you go to your site url + /sitemap.xml. This document is generated automatically every 24 hours. I fetch this .xml map and make a html structure which I populate with the begotten information. In the process there is a bit of sorting going on to put things into categories and make them look presentable. To do this I fetch the JSON for each individual link, begotten by appending ?format=json-pretty to the url. From there I glean the well formatted title of the page, as it is not found in the xml document. Note that for shop categories I format the title by cleaning up the link, as the JSON of shop categories doesn’t contain a title, unless you want to iterate through each category which I didn’t like. As for putting the links into categories, in my case three of them, it is done manually through sorting by keywords and storing the remainder in the last category. As for the visual interface, it is a row of houses, a street if you like. Each house corresponds to a link above. The search functions alike my other instant searches, except this one also hides empty categories and houses after filtering the results.
HTML: <div id="mapSearchInputContainer" class="mapSearchInputContainer"> <input type="text" id="mapSearchInput" class="map-search-input" placeholder=""> </div> <div id="sitemaphouse-image-container"></div>
JAVASCRIPT: <script> document.addEventListener('DOMContentLoaded', function() { const subString = '/sitemap'; const getURL = window.location.href; const mapSearchInputContainer = document.querySelector('#mapSearchInputContainer'); const containerHouses = document.getElementById('sitemaphouse-image-container'); if (getURL.includes(subString)) { // Array of image URLs const imageArray = [ 'https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/09531fcb-25ed-4aaa-86cd-4ca1d685f938/House19.png', 'https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/828970b0-b68b-4201-94b4-1dfbc5e979cd/House20.png', 'https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/604324af-0d6b-4f16-8dc7-c7e77d8e343c/House21.png' ]; //embed the map const sitemapPlaceTextBlock = document.getElementById('block-yui_3_17_2_1_1725267087091_56500'); const sitemapPlaceText = sitemapPlaceTextBlock.querySelector('p'); sitemapPlaceText.textContent = ''; const containerMain = document.createElement('div'); containerMain.id = 'sitemap-container-main'; const container = document.createElement('div'); container.id = 'sitemap-container'; containerMain.appendChild(container); sitemapPlaceTextBlock.appendChild(containerMain); //embed houses const housesPlaceTextBlock = document.getElementById('block-69cee297babf88af4600'); const housesPlaceText = housesPlaceTextBlock.querySelector('p'); housesPlaceText.textContent = ''; housesPlaceTextBlock.appendChild(containerHouses); //filter and structuring options const sitemapUrl = 'https://www.polivantage.com/sitemap.xml'; const excludedPatterns = ['/404', '/shop/p/', '/developmentlog', '/sitemap']; const categoryPatterns = { 'Main:': ['/history', '/home', '/services', '/contact', '/legal', '/devlog', '/art'], 'Shop:': ['/shop'], 'Art:': [] }; const categoryOrder = ['Main:', 'Shop:', 'Art:']; //to highlight links by house function highlightLink(imageLink) { const textLinks = container.querySelectorAll('a'); textLinks.forEach(textLink => { if (textLink.href === imageLink) { textLink.style.color = '#ea4b1a'; } else { textLink.style.color = ''; } }); } //to fetch and format the title from the JSON endpoint async function getDisplayTextFromJson(url) { try { const response = await fetch(`${url}?format=json-pretty`); const jsonData = await response.json(); return jsonData.collection.title || url; } catch (error) { console.error('Error fetching JSON data:', error); return url; // Fallback to URL if there is an error } } //retrieve the map and filter it fetch(sitemapUrl) .then(response => response.text()) .then(str => new window.DOMParser().parseFromString(str, "text/xml")) .then(async data => { const urls = data.querySelectorAll('url'); let imageIndex = 0; const categories = {}; const houseImageMapping = []; //format the display text using jsons const titlePromises = Array.from(urls).map(async url => { const loc = url.querySelector('loc').textContent; let exclude = false; excludedPatterns.forEach(pattern => { if (loc.includes(pattern)) { exclude = true; } }); if (exclude) return; let displayText; //shop category will not use json to format text if (categoryPatterns['Shop:'].some(pattern => loc.includes(pattern))) { displayText = loc.replace('https://www.polivantage.com/', ''); displayText = displayText.replace('developmentlog/', ''); displayText = displayText.replace('shop/', ''); displayText = displayText.charAt(0).toUpperCase() + displayText.slice(1); } else { displayText = await getDisplayTextFromJson(loc); } let category = 'Art:'; for (const [cat, patterns] of Object.entries(categoryPatterns)) { if (patterns.some(pattern => loc.includes(pattern))) { category = cat; break; } } if (!categories[category]) { categories[category] = []; } categories[category].push({ href: loc, text: displayText }); //create houses const imageWrapper = document.createElement('div'); imageWrapper.className = 'sitemaphouse-image-wrapper'; const img = document.createElement('img'); img.src = imageArray[imageIndex % imageArray.length]; img.alt = displayText; houseImageMapping.push({ link: loc, imgWrapper: imageWrapper }); imageWrapper.addEventListener('click', function() { window.location.href = loc; }); imageWrapper.addEventListener('mouseover', () => highlightLink(loc)); imageWrapper.addEventListener('mouseout', () => highlightLink('')); imageWrapper.appendChild(img); img.classList.add('loaded'); const randomScale = 1 + Math.random() * 0.3; imageWrapper.style.minWidth = `${randomScale * 150}px`; imageIndex++; }); //create html structure await Promise.all(titlePromises); let htmlContent = '<div class="category-main-container">'; categoryOrder.forEach(category => { if (categories[category]) { const categoryClass = 'category-' + category.replace(/:/g, '').toLowerCase(); htmlContent += `<div class="category-container ${categoryClass}"> <h4>${category}</h4><ul>`; categories[category].forEach(link => { htmlContent += `<li><a href="${link.href}">${link.text}</a></li>`; const house = houseImageMapping.find(h => h.link === link.href); if (house) { containerHouses.appendChild(house.imgWrapper); } }); htmlContent += `</ul></div>`; } }); htmlContent += '</div>'; container.innerHTML = htmlContent; container.style.minWidth = `${container.offsetWidth}px`; }) .catch(error => console.error('Error fetching sitemap:', error)); //embed the search const mapSearchPlaceTextBlock = document.getElementById('block-79e0b6b6e5aa365774d8'); const mapSearchPlaceText = mapSearchPlaceTextBlock.querySelector('p'); mapSearchPlaceText.textContent = ''; mapSearchPlaceTextBlock.appendChild(mapSearchInputContainer); //handle search const mapSearchInput = document.querySelector('#mapSearchInput'); const debounceDelay = 700; let debounceTimeout; mapSearchInput.addEventListener('input', function() { clearTimeout(debounceTimeout); debounceTimeout = setTimeout(() => { const titles = container.querySelectorAll('a'); const query = this.value.toLowerCase(); titles.forEach(title => { const titleText = title.textContent.toLowerCase(); const listItem = title.closest('li'); if (titleText.includes(query)) { listItem.style.display = ''; } else { listItem.style.display = 'none'; } }); // Filter house images const houseImages = containerHouses.querySelectorAll('img'); houseImages.forEach(img => { const imgAlt = img.alt.toLowerCase(); const wrapper = img.parentElement; wrapper.style.display = imgAlt.includes(query) ? '' : 'none'; }); //hide empty categories const allCategories = container.querySelectorAll('h4'); allCategories.forEach(category => { const categoryList = category.nextElementSibling; const visibleItems = categoryList.querySelectorAll('li:not([style*="display: none"])'); if (visibleItems.length === 0) { category.style.display = 'none'; categoryList.style.display = 'none'; } else { category.style.display = ''; categoryList.style.display = ''; } }); }, debounceDelay); }); //clear input if click outside of search depending on input value var hasText = false; function checkInput() { hasText = mapSearchInput.value.trim() !== ''; } document.addEventListener('click', function(event) { checkInput(); if (event.target !== mapSearchInput && hasText == false) { mapSearchInput.classList.remove('texted'); } else { mapSearchInput.classList.add('texted'); } }); //houses scroll behavior if (!containerHouses) return; let scrollInterval; const maxScrollSpeed = 120; // Maximum scroll speed (pixels per frame) const buffer = 500; // Distance from the edge where scroll starts const scrollCheckInterval = 20; // Interval to check scroll speed let isScrolling = false; function calculateScrollSpeed(mouseX, containerWidth) { const scrollArea = containerWidth / 2 - buffer; const distanceFromLeftEdge = mouseX - buffer; const distanceFromRightEdge = containerWidth - mouseX - buffer; let scrollSpeed = 0; if (distanceFromLeftEdge < 0) { scrollSpeed = -Math.min(maxScrollSpeed, (-distanceFromLeftEdge / scrollArea) * maxScrollSpeed); } else if (distanceFromRightEdge < 0) { scrollSpeed = Math.min(maxScrollSpeed, (-distanceFromRightEdge / scrollArea) * maxScrollSpeed); } return scrollSpeed; } function startScrolling() { if (isScrolling) return; // Prevent multiple intervals from being set isScrolling = true; scrollInterval = setInterval(function () { const containerRect = containerHouses.getBoundingClientRect(); const mouseX = window.mouseX - containerRect.left; const scrollSpeed = calculateScrollSpeed(mouseX, containerRect.width); if (scrollSpeed !== 0) { containerHouses.scrollLeft += scrollSpeed; } else { stopScrolling(); } }, scrollCheckInterval); } function stopScrolling() { clearInterval(scrollInterval); isScrolling = false; } function updateMousePosition(event) { window.mouseX = event.clientX; } containerHouses.addEventListener('mousemove', function (event) { updateMousePosition(event); if (!isScrolling) { startScrolling(); } }); containerHouses.addEventListener('mouseenter', function (event) { updateMousePosition(event); startScrolling(); }); containerHouses.addEventListener('mouseleave', function () { stopScrolling(); }); //exit } else { mapSearchInputContainer.remove(); containerHouses.remove(); } }); </script>
CSS: //DYNAMIC SITE MAP// //houses #sitemaphouse-image-container { max-width: none; overflow-x: auto; white-space: nowrap; border: 0; padding-left: 10px; padding-right: 10px; padding-bottom: 20px; margin-top: 20px; display: flex; align-items: flex-end; justify-content: flex-start; scroll-behavior: smooth; position: relative; border-left: solid 2px; border-right: solid 2px; border-image: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 35%, rgba(0,0,0,1) 65%, rgba(0,0,0,0) 100%); border-image-slice: 1; height: 350px; } .sitemaphouse-image-wrapper { display: block; margin: 0; min-width: 150px; max-width: 270px; margin-right: 5px; margin-left: 5px; cursor: pointer; transition: all 0.3s ease; } .sitemaphouse-image-wrapper:hover { scale: 1.12; } .sitemaphouse-image-wrapper img { display: block; width: 100%; height: 100%; height: auto; pointer-events: auto; } //map links #sitemap-container-main { display: flex; justify-content: center; height: auto; } #sitemap-container { } .category-main-container { border-top: solid 2px; border-bottom: solid 2px; border-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 35%, rgba(0,0,0,1) 65%, rgba(0,0,0,0) 100%); border-image-slice: 1; display: grid; grid-template-columns: repeat(3, auto); gap: 20px; } .category-container { max-height: 500px; overflow-y: auto; padding-left: 20px; padding-right: 40px; } .category-art ul { columns: 3; } .category-shop ul { } .category-main ul { } #sitemap-container ul { padding: 0; } #sitemap-container li { padding-bottom: 5px; } #sitemap-container a { } #sitemap-container a:hover { } #sitemap-container h4 { padding: 0; margin: 0; margin-top: 30px; } //map search #mapSearchInputContainer { max-width: 2200px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; height: 50px; margin: 0; padding: 0; top: -10px; } #mapSearchInput { width: 500px; height: 50px !important; display: block; background: transparent; border: 0 !important; font-size: 1.3rem; letter-spacing: 1.8px; outline: none; padding: 0; margin: 0; text-align: left; z-index: 999; transition: all .7s ease !important; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/2b60bbec-4d1c-4d73-9d6a-009b9bf1d26a/SearchCrosshairIcon.png); background-size: 50px; background-position: bottom 0px left 50%; background-repeat: no-repeat; padding-left: 50px !important; } @media screen and (max-width:790px) { #mapSearchInput { width: 90%; } } #mapSearchInput:focus { outline: none; transition: all .7s ease; background-position: bottom 0px left -6px; } #mapSearchInput.texted { outline: none; transition: all .7s ease; background-position: bottom 0px left -6px; }
The empty CSS is for you to style if you like, it includes the individual categories. Recently I started putting my code into individual pages, instead of piling it up in the header code injection, however all my code is built with checks, so it will function just as well in the header.
COMMENTS:
Leave a comment: