Shopify Basic SEO Audit

Shopify

$50

7 Days

  • Quick Site Health Check
  • Identifies Key SEO Issues
Learn More

Squarespace SEO Starter

Squarespace

$100

14 Days

  • Keyword & Structure Optimization
  • Foundational SEO Elements
Learn More

WooCommerce Product Page Power-Up

WooCommerce

$150

14 Days

  • Boost Product Page Visibility
  • SEO-Friendly Product Descriptions
Learn More

Shopify Growth Package

Shopify

$300

30 Days (+90 Day Tracking)

  • Scale Your Shopify Business
  • Comprehensive SEO Solution
Learn More

WooCommerce SEO Domination

WordPress (WooCommerce)

$400

30 Days (+120 Day Tracking)

  • Outrank Competitors
  • Capture Maximum Market Share
Learn More

Squarespace Complete SEO & Content Strategy

Squarespace

$500

30 Days (+180 Day Tracking)

  • Turn Squarespace into Lead Gen
  • SEO & Content Strategy Power
Learn More

Integrate an Interactive "Latest Posts" Widget with Pinning Feature (Multi-Platform Guide)

No comments

Install an Interactive Latest Posts Widget with Persistent Visual Pinning (Multi-Platform Guide)

Is your website's sidebar more of a static billboard than an engaging entry point? You work hard creating amazing content, but if visitors aren't easily discovering your latest or most important posts, you're missing out on valuable engagement opportunities. Standard "Recent Posts" lists often get overlooked, blending into the background and failing to capture attention in today's fast-paced digital world.

What if you could transform that passive space into a dynamic discovery tool?

Imagine offering your visitors more than just titles. Picture a "Latest Posts" widget showcasing eye-catching thumbnails, revealing quick "Read" or "Pin" actions on hover, and even remembering which posts a user visually pinned when they return to your site! This isn't just about aesthetics; it's about creating an interactive element that encourages exploration and helps users connect with the most relevant content.

This comprehensive guide is your key to unlocking that potential. We provide the complete code and step-by-step integration instructions for an enhanced "Latest Posts" widget featuring:

  • Attractive Thumbnails: Catch the eye instantly.

  • Interactive Hover Actions: Offer quick "Read" and "Pin" options.

  • Persistent Visual Pinning: Uses browser storage so users' pinned choices are remembered across visits (on the same browser).

  • CMS Control: You can pre-define featured posts using simple tags or labels in your backend (like Blogger, WordPress, Shopify, Joomla, Drupal, Squarespace, etc.).

Whether you're running a custom-built site or using a popular CMS, this guide will walk you through adding this engagement-boosting feature. Let's turn your sidebar into a content-converting powerhouse!

Let's get this powerful tool, "Latest Posts" Widget with Pinning Feature, working on your site!

The Widget Code of the latest post pinning:

First, here is the complete code block you'll need. It includes the necessary HTML structure, the JavaScript logic for fetching posts and handling the interactive features, and the CSS for styling. Copy this entire block.

<!-- Latest Posts Widget with Thumbnails, Hover Actions & VISUAL PERSISTENT Pinning (Bootstrap Enhanced) --> <div class="recent-posts-widget card mb-4"> <div class="card-header bg-primary text-white"> Latest Posts </div> <div class="card-body"> <ul id="recent-posts-with-thumbs" class="list-unstyled"> <!-- Loading Indicator (Optional but Recommended) --> <li id="loading-indicator" style="text-align: center; padding: 15px; color: #6c757d;">Loading posts...</li> <!-- The latest posts will load here --> </ul> <div class="widget-credit"> Widget by <a href="https://www.seosiri.com/" target="_blank" rel="noopener noreferrer" title="SEO & Content Strategy Insights">SEOsiri.com</a> </div> </div> <!-- Closing card-body --> <script type="text/javascript"> //<![CDATA[ // --- Configuration --- const pinnedLabel = "Pinned"; // <<< CHANGE THIS Label to match the one you use in your CMS const numberOfPosts = 5; // How many total posts to show const defaultThumb = "https://via.placeholder.com/72"; // Default thumbnail const localStorageKey = 'userPinnedPostsWidget'; // Key for storing pinned IDs // --- End Configuration --- function recentPostsWithThumbs(json) { const postList = document.getElementById("recent-posts-with-thumbs"); const loadingIndicator = document.getElementById("loading-indicator"); postList.innerHTML = ''; // Clear // --- Feed Structure Check (Basic) --- // Adapt this check based on YOUR CMS's feed/API response structure if (!json || !json.feed || !json.feed.entry || json.feed.entry.length === 0) { postList.innerHTML = '<li style="text-align: center; color: #6c757d;">No posts found or invalid feed format.</li>'; console.warn("Feed data missing or in unexpected format:", json); return; } // --- End Feed Structure Check --- // --- Get user's locally pinned posts --- let userPinnedIds = new Set(); try { userPinnedIds = new Set(JSON.parse(localStorage.getItem(localStorageKey) || '[]')); } catch (e) { console.error("Error reading pinned posts from localStorage:", e); localStorage.removeItem(localStorageKey); // Clear corrupted data } // --- const entries = json.feed.entry; // Assumes Blogger structure, **ADAPT FOR YOUR CMS** let cmsPinnedPosts = []; // Store entries pinned via CMS label let regularPosts = []; // Store other entries let postsDataMap = new Map(); // Store data for easier lookup later let postsProcessed = 0; for (let i = 0; i < entries.length && postsProcessed < (numberOfPosts + 10); i++) { // Fetch more to ensure enough non-pinned available const entry = entries[i]; // --- Data Extraction (NEEDS ADAPTATION FOR YOUR SPECIFIC CMS) --- const postTitle = (entry.title && entry.title.$t) ? entry.title.$t : null; // Use null if no title if (!postTitle) continue; // Skip entries without titles const postDate = (entry.published && entry.published.$t) ? entry.published.$t.substring(0, 10) : ""; let postLink = '#'; let postImage = defaultThumb; let postId = `post-${i}`; // Fallback ID if (entry.link) { // Adapt link extraction for (let j = 0; j < entry.link.length; j++) { if (entry.link[j].rel == "alternate") { postLink = entry.link[j].href; try { // Create a more stable ID const urlParts = postLink.split('/'); const lastPart = urlParts[urlParts.length - 1]; if (lastPart) postId = `post-${lastPart.split('.')[0].replace(/[^a-zA-Z0-9-_]/g, '')}`; } catch(e) { /* Ignore */ } break; } } } if (entry.media$thumbnail && entry.media$thumbnail.url) { // Adapt thumbnail extraction postImage = entry.media$thumbnail.url.replace(/\/s\d+(\-c)?\//, '/s100-c/'); } const hasCMSPinnedLabel = (entry.category && Array.isArray(entry.category)) ? entry.category.some(cat => cat.term === pinnedLabel) : false; // Adapt category/tag check // --- End Data Extraction Adaptation --- const postData = { postId, postTitle, postDate, postLink, postImage, hasCMSPinnedLabel }; postsDataMap.set(postId, postData); // Store data by ID if (hasCMSPinnedLabel) { cmsPinnedPosts.push(postData); } else { regularPosts.push(postData); } postsProcessed++; } // --- Combine and Sort Posts --- let combinedPosts = []; // 1. Add posts pinned by user (from localStorage) that WEREN'T pinned by CMS userPinnedIds.forEach(id => { if (postsDataMap.has(id) && !postsDataMap.get(id).hasCMSPinnedLabel) { combinedPosts.push(postsDataMap.get(id)); postsDataMap.delete(id); // Remove from map to avoid duplicates } }); // 2. Add posts pinned by CMS (already separated) cmsPinnedPosts.forEach(postData => { if (postsDataMap.has(postData.postId)) { // Check if not already added by user pin combinedPosts.push(postData); postsDataMap.delete(postData.postId); } }); // 3. Add remaining regular posts until numberOfPosts is reached regularPosts.forEach(postData => { if (combinedPosts.length < numberOfPosts && postsDataMap.has(postData.postId)) { combinedPosts.push(postData); postsDataMap.delete(postData.postId); } }); // Ensure we don't exceed numberOfPosts due to pinning logic combinedPosts = combinedPosts.slice(0, numberOfPosts); // --- Build Final HTML --- let finalHTML = ''; combinedPosts.forEach(postData => { const isVisuallyPinned = postData.hasCMSPinnedLabel || userPinnedIds.has(postData.postId); const liClasses = `media mb-3 post-item has-actions-on-hover ${isVisuallyPinned ? 'pinned-post' : ''}`; const pinIconHTML = isVisuallyPinned ? '<span class="pin-icon" aria-hidden="true">📌</span> ' : ''; const pinButtonText = isVisuallyPinned ? 'Unpin' : 'Pin'; finalHTML += ` <li class="${liClasses}" data-post-id="${postData.postId}"> <div class="post-content-area"> <a href="${postData.postLink}" tabindex="-1"> <img src="${postData.postImage}" class="mr-3 rounded post-thumbnail" alt="${postData.postTitle}" width="72" height="72" loading="lazy"> </a> <div class="media-body post-details"> <h6 class="mt-0 mb-1 post-title"> ${pinIconHTML} <a href="${postData.postLink}" class="text-decoration-none post-title-link">${postData.postTitle}</a> </h6> <small class="text-muted post-date">${postData.postDate}</small> </div> </div> <div class="post-actions"> <a href="${postData.postLink}" class="btn btn-sm btn-outline-primary action-button read-button">Read</a> <button class="btn btn-sm ${isVisuallyPinned ? 'btn-outline-danger' : 'btn-outline-secondary'} action-button pin-toggle-button" data-post-id="${postData.postId}" aria-label="${pinButtonText} Post">${pinButtonText}</button> </div> </li>`; }); postList.innerHTML = finalHTML; // Attach Event Listeners attachPinToggleListeners(); } // Function to Attach Event Listeners for Pinning (with localStorage) function attachPinToggleListeners() { const listContainer = document.getElementById("recent-posts-with-thumbs"); if (!listContainer) return; let userPinnedIds = new Set(JSON.parse(localStorage.getItem(localStorageKey) || '[]')); listContainer.addEventListener('click', function(event) { if (event.target.classList.contains('pin-toggle-button')) { event.preventDefault(); const button = event.target; const targetPostId = button.getAttribute('data-post-id'); const listItem = listContainer.querySelector(`li[data-post-id="${targetPostId}"]`); if (listItem) { const isBecomingPinned = !listItem.classList.contains('pinned-post'); listItem.classList.toggle('pinned-post'); const postTitleElement = listItem.querySelector('.post-title'); if (!postTitleElement) return; if (isBecomingPinned) { userPinnedIds.add(targetPostId); // Add to Set // Add icon if (!postTitleElement.querySelector('.pin-icon')) { const newPinIcon = document.createElement('span'); newPinIcon.className = 'pin-icon'; newPinIcon.setAttribute('aria-hidden', 'true'); newPinIcon.innerHTML = '📌 '; // Add space after postTitleElement.insertBefore(newPinIcon, postTitleElement.firstChild); } // Update button button.textContent = 'Unpin'; button.setAttribute('aria-label', 'Unpin Post'); button.classList.remove('btn-outline-secondary'); button.classList.add('btn-outline-danger'); // Visually move to top if(listContainer.firstChild) listContainer.insertBefore(listItem, listContainer.firstChild); } else { userPinnedIds.delete(targetPostId); // Remove from Set // Remove icon const pinIconSpan = postTitleElement.querySelector('.pin-icon'); if (pinIconSpan) pinIconSpan.remove(); // Update button button.textContent = 'Pin'; button.setAttribute('aria-label', 'Pin Post'); button.classList.remove('btn-outline-danger'); button.classList.add('btn-outline-secondary'); // Optional: Move back down or let reload handle order } // Save updated Set to localStorage try { localStorage.setItem(localStorageKey, JSON.stringify(Array.from(userPinnedIds))); } catch (e) { console.error("Error saving pinned posts to localStorage:", e); // Handle potential storage quota errors } console.log(`Saved pinned IDs: ${localStorage.getItem(localStorageKey)}`); } } }); } // --- Fetch Posts (NEEDS CMS-SPECIFIC ADAPTATION) --- var script = document.createElement('script'); var feedUrl; try { // Use origin for potentially custom domains feedUrl = window.location.origin + `/feeds/posts/default?orderby=published&alt=json-in-script&callback=recentPostsWithThumbs&max-results=${numberOfPosts + 10}`; // Fetch more for sorting flexibility } catch (e) { feedUrl = `/feeds/posts/default?orderby=published&alt=json-in-script&callback=recentPostsWithThumbs&max-results=${numberOfPosts + 10}`; } // --- !! IMPORTANT !! --- Adapt this section if NOT using Blogger JSON-P --- // (Keep commented out fetch examples for WP/Custom API if needed) // --- This part runs ONLY if using the Blogger JSON-P method --- // If using fetch for WP/Other API, ensure the script.src line below is deleted or commented out if (typeof fetch === 'undefined' || !feedUrl.includes('/wp-json/')) { // Basic check to avoid running if fetch is intended console.log("Using JSON-P method. Attempting to fetch feed from:", feedUrl); script.src = feedUrl; script.onerror = function() { const postList = document.getElementById("recent-posts-with-thumbs"); if (postList) postList.innerHTML = '<li style="text-align: center; color: #dc3545;">Error loading posts feed. Check console or feed URL.</li>'; console.error("Failed to load script from:", feedUrl); }; document.getElementsByTagName('head')[0].appendChild(script); } //]]> </script> <style> /* --- Styles (Includes general, hover, pinned, responsive, credit) --- */ /* --- General Widget & Base Item Styles --- */ .recent-posts-widget .card-header { font-size: 1.1rem; } .recent-posts-widget .list-unstyled { padding: 0; margin: 0; } .recent-posts-widget .post-item { position: relative; transition: background-color 0.2s ease-in-out; border-bottom: 1px solid #eee; padding: 12px 0; } .recent-posts-widget .post-item:last-child { border-bottom: none; } .recent-posts-widget .post-content-area { display: flex; align-items: flex-start; padding: 0 12px; } .recent-posts-widget .post-thumbnail { flex-shrink: 0; object-fit: cover; border-radius: 0.25rem; margin-right: 12px; width: 72px; height: 72px;} /* Fixed width/height */ .recent-posts-widget .media-body { flex-grow: 1; min-width: 0; } .recent-posts-widget .post-title { margin-bottom: 0.2rem !important; line-height: 1.3; } .recent-posts-widget .post-title-link { color: #212529; text-decoration: none; font-weight: 500; } .recent-posts-widget .post-title-link:hover { color: #0d6efd; text-decoration: underline; } .recent-posts-widget .post-date { font-size: 0.8em; color: #6c757d; display: block; } /* --- Hover Actions Styles --- */ .post-actions { position: absolute; bottom: 8px; right: 12px; background-color: rgba(248, 249, 250, 0.95); padding: 4px 6px; border-radius: 5px; border: 1px solid #dee2e6; opacity: 0; visibility: hidden; transform: translateY(5px); transition: opacity 0.2s ease-out, visibility 0.2s ease-out, transform 0.2s ease-out; display: flex; gap: 6px; z-index: 5; } .post-item:hover .post-actions { opacity: 1; visibility: visible; transform: translateY(0); } .action-button { font-size: 0.75rem; padding: 0.15rem 0.4rem; cursor: pointer; } /* --- Pinned Post Specific Styles --- */ .recent-posts-widget .pinned-post { background-color: #e7f1ff; border-left: 4px solid #0d6efd; margin-left: -1.25rem; margin-right: -1.25rem; margin-top: -1px; border-bottom: 1px solid #cfe2ff; border-top: 1px solid #cfe2ff; } .recent-posts-widget .pinned-post .post-content-area { padding: 12px 12px 12px 8px; } .recent-posts-widget .pinned-post:first-child { margin-top: -1.25rem; border-top: none; } .recent-posts-widget .pinned-post:last-of-type { border-bottom: 1px solid #cfe2ff; } .recent-posts-widget .pinned-post + .post-item:not(.pinned-post) { border-top: 1px solid #eee; margin-top: 0; } .pin-icon { display: inline-block; margin-right: 5px; color: #0d6efd; font-size: 0.9em; vertical-align: baseline; } .pinned-post .post-title a { font-weight: 600; color: #0a58ca; } .pinned-post:hover { background-color: #dbeaff; } /* --- Responsive Adjustments --- */ @media (max-width: 768px) { .recent-posts-widget .post-thumbnail { width: 55px; height: 55px; margin-right: 10px; } .recent-posts-widget .media-body h6 { font-size: 0.85rem; } .recent-posts-widget .post-date { font-size: 0.75em; } .action-button { font-size: 0.7rem; } .post-actions { bottom: 5px; right: 5px; } .recent-posts-widget .pinned-post { margin-left: -1rem; margin-right: -1rem; } .recent-posts-widget .pinned-post:first-child { margin-top: -1rem; } .recent-posts-widget .pinned-post .post-content-area { padding: 10px 10px 10px 6px; } } @media (min-width: 992px) { .recent-posts-widget .card-header { font-size: 1.2rem; } .recent-posts-widget .post-thumbnail { width: 72px; height: 72px; } .recent-posts-widget .media-body h6 { font-size: 0.95rem; } } /* --- Style for the Credit Link --- */ .widget-credit { font-size: 0.75em; text-align: right; margin-top: 10px; padding-top: 5px; border-top: 1px solid #eee; color: #6c757d; } .widget-credit a { color: #0d6efd; text-decoration: none; } .widget-credit a:hover { text-decoration: underline; } </style> </div> <!-- End Widget -->
 

How to Add This Interactive Widget to Your Website (Smart Guide)

Integrating this dynamic widget can significantly boost engagement. Follow these steps carefully, paying close attention to the platform-specific adaptations required.

Step 1: Copy the Full Widget Code

  • Use the "Copy Code" button above to grab the entire HTML, JavaScript, and CSS code block for the widget. This includes the main <div>, the <script> section, and the <style> section.

Step 2: Configure Basic Settings (Inside the Copied Code)

  • Find the // --- Configuration --- section near the top of the <script> block you just copied.

  • pinnedLabel = "Pinned";: CRITICAL: Change "Pinned" to the exact tag, category, or label name you use within your website's CMS to identify posts you want initially pinned. Remember, it's case-sensitive!

  • numberOfPosts = 5;: Adjust this number to control how many total posts appear in the widget.

  • defaultThumb = "...";: If desired, replace the placeholder URL with a link to your preferred default thumbnail image.

  • localStorageKey = 'userPinnedPostsWidget';: You generally don't need to change this unless you have multiple similar widgets and need unique storage keys.

Step 3: Adapt the Data Source & Parsing (THE MOST IMPORTANT STEP!)

  • The Challenge: The provided JavaScript is pre-configured for Blogger's specific JSON feed format. It needs modification to work with other platforms like WordPress, Shopify, Squarespace, Joomla, Drupal, or your custom site.

  • Your Task:

    1. Identify Your Platform's Data Source: Find out how your specific CMS provides a list of recent posts, including titles, links, publication dates, featured image URLs, and tags/categories, preferably in JSON format. Common sources are platform APIs (like WP REST API) or specialized feeds/plugins. Consult your platform's documentation or developer resources.

    2. Modify the JavaScript Fetch Method:

      • Locate the // --- Fetch Posts --- section in the script.

      • If using Blogger: The existing script.src = feedUrl; method (Option 1) should work (check custom domain configurations).

      • If using WordPress, Shopify, Custom API, etc.: You must comment out or delete the Blogger script.src method. Uncomment and adapt the fetch() examples (Option 2 or 3). Replace the placeholder feedUrl with your actual API endpoint URL. You'll also need to ensure the fetch call correctly triggers the recentPostsWithThumbs function with the received JSON data (e.g., .then(data => recentPostsWithThumbs(data)); - important: adjust the data variable if your API wraps the posts array, e.g., recentPostsWithThumbs({ feed: { entry: data } }) to mimic Blogger's structure if you don't want to change the parsing function heavily).

    3. Modify the JavaScript Data Parsing:

      • Locate the // --- Data Extraction --- section inside the for loop within the recentPostsWithThumbs function.

      • Crucially, change the property accessors (e.g., entry.title.$t, entry.link[j].href, entry.media$thumbnail.url, entry.category.some(...)) to match the exact names and structure of the data fields in the JSON response from your platform's API/feed. Use your browser's Developer Tools (Network tab) to inspect the JSON response or consult the API documentation.

Step 4: Insert the Modified Code into Your Platform

  • Choose the best method for your site:

    • Blogger: Layout -> Add a Gadget -> HTML/JavaScript -> Paste.

    • WordPress: Appearance -> Widgets -> Custom HTML -> Paste. (Or advanced: theme files/code snippets).

    • Shopify: Online Store -> Themes -> Edit code -> Add as Custom Content section or paste into relevant .liquid file.

    • Squarespace: Use a Code Block (HTML mode). Check plan limits.

    • Joomla/Drupal: Use a Custom HTML module/block. Ensure scripts/styles aren't stripped.

    • Custom Site: Paste HTML div where needed. Add <script> before </body>. Add <style> to <head> or the main CSS file.

Step 5: Tag/Label Posts in Your CMS

  • Log in to your website's backend/CMS.

  • Edit the posts you wish to feature at the top of the widget initially.

  • Add the exact tag or label (case-sensitive) defined for pinnedLabel in Step 2. Save the posts.

Step 6: Test, Troubleshoot, and Refine

  • Clear Caches: Crucial after adding code.

  • Verify Loading: Check if the widget appears. Use the Browser Developer Console (F12 -> Console) for errors (feed URL, parsing issues, etc.).

  • Check Pinning: Confirm labeled posts appear first with pinned styling.

  • Test Interactions: Hover, click "Read," click "Pin/Unpin." Reload the page and verify that posts you visually pinned stay pinned and at the top (thanks to localStorage). Check localStorage in DevTools (Application tab).

  • Responsiveness: Test on various screen sizes.

  • CSS Conflicts: Use browser developer tools (Inspector) to diagnose styling issues caused by theme conflicts. Adjust widget CSS specificity if needed.

(How Persistent Pinning Works - Important Note)

The "Pin/Unpin" functionality in this widget uses browser localStorage. This means:

  • It's Visual & Persistent (Per Browser): When a user clicks "Pin," the post looks pinned and stays that way even after reloading the page in that specific browser.

  • It Does NOT Change Backend Labels: Clicking the button does not add or remove the actual tag/label in your CMS (Blogger, WordPress, etc.). The initial pinned state on page load is always determined by the labels you set in your CMS.

  • Browser/Device Specific: Preferences are saved only in the browser where the action was taken.

Just double-check:

Does the "How Persistent Pinning Works" section in the blog post you're looking at mention localStorage and explain that the visual pinning should persist after a reload? If yes, you have the correct guide content.

Does the "Testing" section mention reloading the page and checking localStorage to verify persistence? If yes, you have the correct guide content.

Adding dynamic elements like this interactive "Latest Posts" widget with persistent visual pinning can significantly elevate the user experience on your site. It encourages deeper content exploration, effectively highlights crucial articles, and gives visitors a sense of personalized control. While setup requires careful attention to platform-specific data sources, the payoff in increased engagement is substantial.

At SEOsiri.com, we understand that effective SEO isn't just about keywords; it's about creating positive user experiences that keep visitors engaged and signal value to search engines. Tools like this widget, focused on improving content discoverability and user interaction, directly support a human-centered SEO philosophy. When you make your site better for users, search engines notice.

We provide the complete code and detailed, step-by-step instructions to integrate this powerful widget, whether you're using a popular CMS or a custom-built website. Ready to make your content more discoverable and engaging? Let's get started!

We hope this detailed guide helps you implement this feature successfully! For more advanced insights on enhancing user engagement, content optimization, and technical SEO, explore the wealth of resources available on SEOsiri.com.

Connect with me on Dev.to. Connect with me on Quora. Join me on Web Developers.

Thank you
Momenul Ahmad


Momenul Ahmad

MomenulAhmad: Helping businesses, brands, and professionals with ethical  SEO and digital Marketing. Digital Marketing Writer, Digital Marketing Blog (Founding) Owner at SEOSiri, Pabna, Partner at Brand24, Triple Whale, Shopify, CookieYesAutomattic, Inc.

No comments :

Post a Comment

Get instant comments to approve, give 5 social share (LinkedIn, Twitter, Quora, Facebook, Instagram) follow me (message mentioning social share) on Quora- Momenul Ahmad

Also, never try to prove yourself a spammer and, before commenting on SEOSiri, please must read the SEOSiri Comments Policy

Or,
If you have a die heart dedicated to SEO Copywriting then SEOSiri welcomes you to Guest Post Submission

link promoted marketer, simply submit client's site, here-
SEOSIRI's Marketing Directory