-
-
Notifications
You must be signed in to change notification settings - Fork 70
Open
Description
Running Ruby on a installation with 400+ posts, and get duplicates in the infinite scroll.
I have an update to main.js which resolves it.
Ruby's main.js looks like this in the repo, I'm a little lost on how to create a branch here..

Here is my main.js which uses a Set() to track post ID and prevent duplicates, feel free to merge it.
(() => {
// Track loaded post IDs to prevent duplicates
const loadedPosts = new Set();
// Initialize pagination
pagination(true);
// Handle mobile menu
const burger = document.querySelector('.gh-burger');
if (burger) {
burger.addEventListener('click', function () {
if (document.body.classList.contains('is-head-open')) {
document.body.classList.remove('is-head-open');
} else {
document.body.classList.add('is-head-open');
}
});
}
// Initialize lightbox for images
lightbox('.kg-image-card > .kg-image[width][height], .kg-gallery-image > img');
// Handle responsive iframes
reframe(document.querySelectorAll([
'.gh-content iframe[src*="youtube.com"]',
'.gh-content iframe[src*="youtube-nocookie.com"]',
'.gh-content iframe[src*="player.vimeo.com"]',
'.gh-content iframe[src*="kickstarter.com"][src*="video.html"]',
'.gh-content object',
'.gh-content embed'
].join(',')));
// Initialize dropdown
dropdown();
/**
* Enhanced pagination function that handles both infinite scroll and load-more button functionality
*
* @param {boolean} isInitial - If true, sets up infinite scroll using IntersectionObserver.
* If false, uses load-more button functionality.
* @param {Function} callback - Optional callback function executed after loading new posts.
* Receives (uniquePosts, nextElement) as parameters.
* @param {boolean} waitForImages - If true, new posts are initially hidden until images load.
* Helps prevent layout shifts during image loading.
*/
function pagination(isInitial, callback, waitForImages = false) {
// Main container for posts
const container = document.querySelector('.gh-feed');
if (!container) return;
// State tracking for preventing concurrent loads
let isLoading = false;
// Element used to trigger infinite scroll
// Falls back through multiple options to find a suitable trigger element
let nextElement = container.nextElementSibling || container.parentElement.nextElementSibling || document.querySelector('.gh-foot');
// Load more button for manual pagination if infinite scroll is disabled
const loadMoreButton = document.querySelector('.gh-loadmore');
// Early cleanup: Remove load more button if there are no more pages to load
if (!document.querySelector('link[rel=next]')) {
loadMoreButton?.remove();
return;
}
/**
* Fetches and appends posts from the next page
* - Fetches HTML from next page URL
* - Parses response and extracts posts
* - Filters out duplicate posts using URL tracking
* - Appends unique posts to container
* - Updates pagination links
* - Handles visibility for image loading
*/
async function loadNextPage() {
const nextPageUrl = document.querySelector('link[rel=next]');
if (!nextPageUrl) return;
try {
const response = await fetch(nextPageUrl.href);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract all posts from next page, excluding featured and related posts
const newPosts = Array.from(doc.querySelectorAll('.gh-feed:not(.gh-featured):not(.gh-related) > *'));
// Remove any posts that have already been loaded to prevent duplicates
// Uses post URLs stored in loadedPosts Set for tracking
const uniquePosts = newPosts.filter(post => {
const postLink = post.querySelector('a.post-link');
if (!postLink) return false;
const postUrl = postLink.href;
if (loadedPosts.has(postUrl)) {
return false;
}
loadedPosts.add(postUrl);
return true;
});
// Use DocumentFragment for better performance when adding multiple posts
// Optionally hide posts initially if waiting for images to load
const fragment = document.createDocumentFragment();
uniquePosts.forEach(post => {
const importedPost = document.importNode(post, true);
if (waitForImages) {
importedPost.style.visibility = 'hidden';
}
fragment.appendChild(importedPost);
});
container.appendChild(fragment);
// Update or remove pagination links based on next page availability
// Removes load more button when reaching the last page
const newNextLink = doc.querySelector('link[rel=next]');
if (newNextLink && newNextLink.href) {
nextPageUrl.href = newNextLink.href;
} else {
nextPageUrl.remove();
loadMoreButton?.remove();
}
// Execute callback with newly loaded posts and next element reference
if (callback) {
callback(uniquePosts, nextElement);
}
} catch (error) {
console.error('Error loading next page:', error);
nextPageUrl.remove();
loadMoreButton?.remove();
}
}
/**
* Handles infinite scroll functionality
* - Checks if more pages are available
* - Prevents concurrent loading
* - Triggers load when next element comes into view
*/
async function handleScroll() {
if (!document.querySelector('link[rel=next]')) return;
if (isLoading) return;
if (nextElement.getBoundingClientRect().top <= window.innerHeight) {
isLoading = true;
await loadNextPage();
isLoading = false;
}
}
/**
* Initialize pagination based on mode (infinite scroll vs load more button)
* For infinite scroll (isInitial = true):
* - Sets up IntersectionObserver to detect when more content should load
* - Handles both immediate and delayed loading based on waitForImages
* For load more button (isInitial = false):
* - Attaches click handler to load more button
*/
if (isInitial) {
const observer = new IntersectionObserver(async function(entries) {
if (isLoading) return;
if (entries[0].isIntersecting) {
isLoading = true;
if (waitForImages) {
await loadNextPage();
} else {
while (nextElement.getBoundingClientRect().top <= window.innerHeight && document.querySelector('link[rel=next]')) {
await loadNextPage();
}
}
isLoading = false;
}
});
observer.observe(nextElement);
} else if (loadMoreButton) {
loadMoreButton.addEventListener('click', loadNextPage);
}
}
})();
Metadata
Metadata
Assignees
Labels
No labels