I've always loved the idea of having a social feed where all your social media posts from the likes of Facebook, Twitter, LinkedIn, Instagram, and the rest would all showcase and be displayed on one single page.
![]() |
Yes more of something like this |
Now, I’m embracing the POSSE concept (Publish on Own Site, Syndicate Elsewhere). Here, I first publish a post and then share it on various social media. I’m on a social break at the moment, but when I resume, the goal is to share more from here outward. I love the sense of control this gives over my online presence, even within the limitations of Blogger.
Introducing My Site’s Social Feed
I just recently created a Social feed Page for this site. The feed showcases various post types: replies, likes, reposts, and bookmarks; alongside regular blog posts with the "syndicated label". This isn’t just for me; I hope it might inspire or help someone else looking to do something similar.
What the Social Feed Does
The social feed acts like a mini social timeline on my Blogger site, showing posts tagged with specific labels that I assign to my posts.
Each "card" in the feed represents a social post type like micro-posts, syndicated posts, replies, likes, and bookmarks, which is filtered using custom labels on Blogger.
It’s visually styled to feel like a modern social feed, with each postcard displaying the author, post date, and content. To create a dynamic, interactive experience, I added filters so visitors can toggle between different post types.
And since I wanted this to feel connected to the broader social ecosystem, the feed includes syndication links when available, so anyone can see where a post has been shared. This was made possible by a JavaScript snippet that searches through the post contents to find the syndicate microformats2 class. This will be further explained later on.
The Essence of this
I guess is just boredom and making Blogger more IndieWeb
I built this to bring some IndieWeb features to my site. I found a way to make different post types/ social-focused content live alongside full-length blog posts.
Blogger doesn’t natively support post types or many interactive features, I used labeled filters and custom JavaScript to separate and display these post types. It’s a small way of giving my site some IndieWeb vibes while making it feel more like an interactive space I control.
How It Works
The feed uses a combination of HTML, CSS, and JavaScript to fetch and display my posts based on the labels I assign. Here’s a basic rundown of how each component works:
1. Fetching and Filtering Posts:
The script fetches posts from Blogger’s JSON feed API and filters them based on labels I use for IndieWeb post types (like `reply`, `like`, `repost`, etc.). Each of these posts has its correct appropriate labels - This is a must
2. Dynamic Grid Layout:
I used a CSS grid to create a responsive layout that adapts based on screen size, so the feed looks good on mobile and desktop.
3. Post Card Design:
Each post is displayed in a card format with elements like the post author, content, date, and any images included. When a post is syndicated to other platforms (like Twitter or Mastodon), I pull in icons for each platform so visitors can follow those links.
4. Load More Button:
Instead of showing all posts at once, I added a “Load More” button to fetch additional posts as visitors scroll. This way, I like it.
The Snippet
Here’s the full code I used for the social feed, open to anyone who wants to try it:
HTML
<div class="filters">
<button class="filter-btn active" data-filter=" all">All</button>
<button class="filter-btn" data-filter=" post">Post</button>
<button class="filter-btn" data-filter="reply">Reply</button>
<button class="filter-btn" data-filter="repost">Repost</button>
<button class="filter-btn" data-filter="like">Like</button>
<button class="filter-btn" data-filter="bookmark">Bookmark</button>
<button class="filter-btn" data-filter="rsvp">RSVP</button>
</div>
<div id="social-feed" class="social-feed h-feed"></div>
<button id="load-more" class="load-more">Load More</button>
CSS
<style>.nightmode .post-card a {color:inherit!important}.c-link, .nightmode .c-link{text-decoration:none!important;color:inherit!important}
.social-feed {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
max-width: 1200px;
margin: 0 auto;
}
@media (max-width: 1024px) {
.social-feed { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 768px) {
.social-feed { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
.social-feed { grid-template-columns: 1fr; }
}.nightmode .post-card{background-color: #33373D}
.post-card {
background-color: inherit;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
overflow: hidden;
transition: box-shadow 0.3s ease;
}
.post-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.post-header {
display: flex;
align-items: center;
padding: 12px;
}
.post-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #e0e0e0;
margin-right: 12px;
}
.post-info {
flex-grow: 1;
}
.post-author {
font-weight: bold;
margin-bottom: 2px;
}
.post-date {
color: #65676B;
font-size: 0.9em;
}
.post-content {
padding: 0 12px 12px;
}
.post-image-container {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
overflow: hidden;
}
.post-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.image-count {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0,0,0,0.6);
color: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 0.8em;
}
.syndication-links {
display: flex;
gap: 10px;
padding: 12px;
border-top: 1px solid #e4e6eb;
}
.syndication-icon {
width: 24px;
height: 24px;
}
.more-link {
padding: 12px;
text-align: right;
color: #1877f2;
font-weight: bold;
text-decoration: none;
}
.load-more {
display: none;
width: 200px;
margin: 20px auto;
padding: 10px;
background-color: #1877f2;
color: white;
text-align: center;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.load-more:hover {
background-color: #166fe5;
}
.filters {
display: flex;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-btn {
margin: 5px;
padding: 8px 16px;
background-color: #e4e6eb;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.filter-btn:hover {
background-color: #d8dadf;
}
.filter-btn.active {
background-color: #1877f2;
color: white;
}
</style>
JavaScript
The Js is the core aspect of this and has not been perfected.
If you want to use you should take note of the marked area.
The first is to change the URL to your site's own
The second marked image part is to be replaced with the author profile image.
The last marked set is to be replaced with image icon links for each profile platform. You will have to host those Icon images somewhere and link to each.
<script>
const feedElement = document.getElementById('social feed);
const loadMoreButton = document.getElementById('load-more');
let currentPage = 1;
const postsPerPage = 20;
let all posts = [];
let currentFilter = 'all';
let displayedPostsCount = 0;
async function fetchPosts(page) {
try {
const response = await fetch(`https://www.sdavidprince.space/feeds/posts/default?alt=json&max-results=${postsPerPage}&start-index=${(page - 1) * postsPerPage + 1}`);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data.feed.entry || [];
} catch (error) {
console.error('Error fetching posts:', error);
return [];
}
}
function createPostCard(post) {
const card = document.createElement('article');
card.className = 'post-card h-entry';
const published = new Date(post.published.$t).toLocaleDateString();
const author = post.author[0].name.$t;
const link = post.link.find(link => link.rel === 'alternate').href;
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
const isSyndicatedOnly = labels.includes('syndicated') && !labels.includes('social');
let content;
let images;
if (isSyndicatedOnly) {
content = post.title.$t;
images = extractImages(post.content.$t);
} else {
content = extractEContent(post.content.$t);
images = extractImages(post.content.$t);
}
const syndicationLinks = extractSyndicationLinks(post.content.$t);
card.innerHTML = `
<div class="post-header">
<div class="post-avatar"><img src="https://blogger.googleusercontent.com/img/a/AVvXsEiX_A56Zyh-QLWDJ-1Ugpp732H8h6l0yuCSXPFl2Nf8N0zUb8WKryx4MQPk0gYtsBW5QWHOWVfGwQawGSGjCVIh0vEsih7bqGGEjO7XVI0aGVC5GWVsRMmTLcmrf3_QeLysrpaAZzXIfA375a5ohMaBXh-Pzq3vA-YVLFHiRwkDRrn0aMu_HnnqD7zCr3Vr=s300"/></div>
<div class="post-info">
<div class="post-author p-author h-card">${author}</div>
<div class="post-date">
<time class="dt-published" datetime="${post.published.$t}">${published}</time>
</div>
</div>
</div>
<a href="${link}" class="u-url c-link"> <div class="post-content">
${isSyndicatedOnly ? `<div class="p-name">${content}</div>` : `<div class="e-content">${content}</div>`}
${images.length > 0 ? `
<div class="post-image-container">
<img class="post-image u-photo" src="${images[0]}" alt="Post image">
${images.length > 1 ? `<div class="image-count">+${images.length - 1}</div>` : ''}
</div>
` : ''}
</div> </a>
<div class="syndication-links">
${syndicationLinks.map(link => `
<a href="${link.url}" class="u-syndication" title="${link.domain}" target="_blank" rel="noopener noreferrer">
<img class="syndication-icon" src="${getSyndicationIcon(link.domain)}" alt="${link.domain}">
</a>
`).join('')}
</div>
<a href="${link}" class="more-link u-url">View Post</a>
`;
return card;
}
function extractEContent(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const eContentElement = doc.querySelector('.e-content');
return eContentElement ? eContentElement.innerHTML : '';
}
function extractImages(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const imgElements = doc.querySelectorAll('img');
return Array.from(imgElements).map(img => img.src);
}
function extractSyndicationLinks(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const syndicationElements = doc.querySelectorAll('a.u-syndication');
return Array.from(syndicationElements).map(a => ({
url: a.href,
domain: new URL(a.href).hostname
}));
}
function getSyndicationIcon(domain) {
const icons = {
'facebook.com': 'https://example.com/facebook-icon.png',
'twitter.com': 'https://example.com/twitter-icon.png',
'linkedin.com': 'https://example.com/linkedin-icon.png',
'mastodon.social': 'https://example.com/mastodon-icon.png',
'medium.com': 'https://example.com/medium-icon.png',
'reddit.com': 'https://example.com/reddit-icon.png'
// other platforms here
};
return icons[domain] || 'https://example.com/default-icon.png';
}
function filterPosts(posts, filter) {
if (filter === 'all') return posts;
return posts.filter(post => {
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
return labels.includes(filter) || (filter === 'post' && !labels.some(label => ['like', 'reply', 'bookmark', 'repost', 'rsvp'].includes(label)));
});
}
async function loadPosts(page) {
const newPosts = await fetchPosts(page);
allPosts = allPosts.concat(newPosts);
const filteredPosts = filterPosts(allPosts, currentFilter);
feedElement.innerHTML = '';
displayedPostsCount = 0;
filteredPosts.forEach(post => {
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
if (labels.includes('social') || labels.includes('syndicated')) {
const card = createPostCard(post);
feedElement.appendChild(card);
displayedPostsCount++;
}
});
loadMoreButton.style.display = displayedPostsCount >= 20 ? 'block' : 'none';
}
loadMoreButton.addEventListener('click', () => {
currentPage++;
loadPosts(currentPage);
});
document.querySelector('.filters').addEventListener('click', (event) => {
if (event.target.classList.contains('filter-btn')) {
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
currentFilter = event.target.dataset.filter;
currentPage = 1;
allPosts = [];
loadPosts(currentPage);
}
});
loadPosts(currentPage);
</script>
Here is other JS versions - Only one in all should be used
<script>
const feedElement = document.getElementById('social-feed');
const loadMoreButton = document.getElementById('load-more');
let currentPage = 1;
const postsPerPage = 20;
let allPosts = [];
let currentFilter = 'all';
let displayedPostsCount = 0;
async function fetchPosts(page) {
try {
const response = await fetch(`https://www.sdavidprince.space/feeds/posts/default?alt=json&max-results=${postsPerPage}&start-index=${(page - 1) * postsPerPage + 1}`);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data.feed.entry || [];
} catch (error) {
console.error('Error fetching posts:', error);
return [];
}
}
function createPostCard(post) {
const card = document.createElement('article');
card.className = 'post-card h-entry';
const published = new Date(post.published.$t).toLocaleDateString();
const author = post.author[0].name.$t;
const link = post.link.find(link => link.rel === 'alternate').href;
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
// Determine post type
const isBookmark = labels.includes('bookmark');
const isSyndicatedOnly = labels.includes('syndicated') && !labels.includes('social');
let content;
let images;
// Handle different post types
if (isBookmark) {
content = post.title.$t; // Only show title for bookmarks
images = []; // No images for bookmarks
} else if (isSyndicatedOnly) {
content = post.title.$t;
images = extractImages(post.content.$t);
} else {
content = extractEContent(post.content.$t);
images = extractImages(post.content.$t);
}
// Extract syndication links
const syndicationLinks = extractSyndicationLinks(post.content.$t);
card.innerHTML = `
<div class="post-header">
<div class="post-avatar"><img src="https://blogger.googleusercontent.com/img/a/AVvXsEiX_A56Zyh-QLWDJ-1Ugpp732H8h6l0yuCSXPFl2Nf8N0zUb8WKryx4MQPk0gYtsBW5QWHOWVfGwQawGSGjCVIh0vEsih7bqGGEjO7XVI0aGVC5GWVsRMmTLcmrf3_QeLysrpaAZzXIfA375a5ohMaBXh-Pzq3vA-YVLFHiRwkDRrn0aMu_HnnqD7zCr3Vr=s300"/></div>
<div class="post-info">
<div class="post-author p-author h-card">${author}</div>
<div class="post-date">
<time class="dt-published" datetime="${post.published.$t}">${published}</time>
</div>
</div>
</div>
<a href="${link}" class="u-url c-link">
<div class="post-content">
${isBookmark
? `<div class="p-name"><strong>🔖 Bookmark:</strong> ${content}</div>`
: isSyndicatedOnly
? `<div class="p-name">${content}</div>`
: `<div class="e-content">${content}</div>`
}
${(!isBookmark && images.length > 0) ? `
<div class="post-image-container">
<img class="post-image u-photo" src="${images[0]}" alt="Post image">
${images.length > 1 ? `<div class="image-count">+${images.length - 1}</div>` : ''}
</div>
` : ''}
</div>
</a>
<div class="syndication-links">
${syndicationLinks.map(link => `
<a href="${link.url}" class="u-syndication" title="${link.domain}" target="_blank" rel="noopener noreferrer">
<img class="syndication-icon" src="${getSyndicationIcon(link.domain)}" alt="${link.domain}">
</a>
`).join('')}
</div>
<a href="${link}" class="more-link u-url">View Post</a>
`;
return card;
}
function extractEContent(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const eContentElement = doc.querySelector('.e-content');
return eContentElement ? eContentElement.innerHTML : '';
}
function extractImages(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const imgElements = doc.querySelectorAll('img');
return Array.from(imgElements).map(img => img.src);
}
function extractSyndicationLinks(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const syndicationElements = doc.querySelectorAll('a.u-syndication');
return Array.from(syndicationElements).map(a => ({
url: a.href,
domain: new URL(a.href).hostname
}));
}
function getSyndicationIcon(domain) {
const icons = {
'facebook.com': 'https://example.com/facebook-icon.png',
'twitter.com': 'https://example.com/twitter-icon.png',
'linkedin.com': 'https://example.com/linkedin-icon.png',
'mastodon.social': 'https://example.com/mastodon-icon.png',
'medium.com': 'https://example.com/medium-icon.png',
'reddit.com': 'https://example.com/reddit-icon.png'
};
return icons[domain] || 'https://example.com/default-icon.png';
}
function filterPosts(posts, filter) {
if (filter === 'all') return posts;
return posts.filter(post => {
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
return labels.includes(filter) || (filter === 'post' && !labels.some(label => ['like', 'reply', 'bookmark', 'repost', 'rsvp'].includes(label)));
});
}
async function loadPosts(page) {
const newPosts = await fetchPosts(page);
allPosts = allPosts.concat(newPosts);
const filteredPosts = filterPosts(allPosts, currentFilter);
feedElement.innerHTML = '';
displayedPostsCount = 0;
filteredPosts.forEach(post => {
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
if (labels.includes('social') || labels.includes('syndicated')) {
const card = createPostCard(post);
feedElement.appendChild(card);
displayedPostsCount++;
}
});
loadMoreButton.style.display = displayedPostsCount >= 20 ? 'block' : 'none';
}
loadMoreButton.addEventListener('click', () => {
currentPage++;
loadPosts(currentPage);
});
document.querySelector('.filters').addEventListener('click', (event) => {
if (event.target.classList.contains('filter-btn')) {
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
currentFilter = event.target.dataset.filter;
currentPage = 1;
allPosts = [];
loadPosts(currentPage);
}
});
loadPosts(currentPage);
</script>
const feedElement = document.getElementById('social-feed');
const loadMoreButton = document.getElementById('load-more');
let currentPage = 1;
const postsPerPage = 20;
let allPosts = [];
let currentFilter = 'all';
let displayedPostsCount = 0;
// Fetch posts from the API
async function fetchPosts(page) {
try {
const response = await fetch(`https://www.sdavidprince.space/feeds/posts/default?alt=json&max-results=${postsPerPage}&start-index=${(page - 1) * postsPerPage + 1}`);
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data.feed.entry || [];
} catch (error) {
console.error('Error fetching posts:', error);
return [];
}
}
// Determine the post type and get appropriate icon/label
function getPostTypeInfo(labels) {
const types = {
bookmark: { icon: '🔖', label: 'Bookmarked' },
like: { icon: '❤️', label: 'Liked' },
reply: { icon: '💬', label: 'Replied to' },
repost: { icon: '🔄', label: 'Reposted' },
rsvp: { icon: '📅', label: 'RSVPed' }
};
for (const [type, info] of Object.entries(types)) {
if (labels.includes(type)) return info;
}
return { icon: '📝', label: 'Posted' }; // Default for regular posts
}
// Extract content based on microformats2 classes
function extractContent(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
// Try to find specific microformats2 content
const eContent = doc.querySelector('.e-content');
const pName = doc.querySelector('.p-name');
const uUrl = doc.querySelector('.u-url');
return {
content: eContent ? eContent.innerHTML : '',
title: pName ? pName.textContent : '',
url: uUrl ? uUrl.href : '',
images: Array.from(doc.querySelectorAll('.u-photo')).map(img => img.src)
};
}
// Create the post card with proper microformats2 markup
function createPostCard(post) {
const card = document.createElement('article');
card.className = 'post-card h-entry';
const published = new Date(post.published.$t);
const author = post.author[0].name.$t;
const link = post.link.find(link => link.rel === 'alternate').href;
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
const postType = getPostTypeInfo(labels);
const extracted = extractContent(post.content.$t);
const isSyndicatedOnly = labels.includes('syndicated') && !labels.includes('social');
// Function to format the published date
const formatDate = (date) => {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Extract syndication links
const syndicationLinks = extractSyndicationLinks(post.content.$t);
card.innerHTML = `
<div class="post-header">
<div class="post-avatar h-card">
<img class="u-photo" src="https://blogger.googleusercontent.com/img/a/AVvXsEiX_A56Zyh-QLWDJ-1Ugpp732H8h6l0yuCSXPFl2Nf8N0zUb8WKryx4MQPk0gYtsBW5QWHOWVfGwQawGSGjCVIh0vEsih7bqGGEjO7XVI0aGVC5GWVsRMmTLcmrf3_QeLysrpaAZzXIfA375a5ohMaBXh-Pzq3vA-YVLFHiRwkDRrn0aMu_HnnqD7zCr3Vr=s300" alt="${author}"/>
</div>
<div class="post-info">
<div class="post-author p-author">${author}</div>
<div class="post-date">
<time class="dt-published" datetime="${published.toISOString()}">${formatDate(published)}</time>
</div>
</div>
</div>
<a href="${link}" class="u-url c-link">
<div class="post-content">
<div class="post-type">
<span class="p-category">${postType.icon} ${postType.label}</span>
</div>
${labels.includes('bookmark') ? `
<div class="p-bookmark-of h-cite">
<div class="p-name">${extracted.title || post.title.$t}</div>
${extracted.url ? `<a href="${extracted.url}" class="u-url">${new URL(extracted.url).hostname}</a>` : ''}
</div>
` : labels.includes('like') ? `
<div class="p-like-of h-cite">
<div class="p-name">${extracted.title || post.title.$t}</div>
${extracted.url ? `<a href="${extracted.url}" class="u-url">${new URL(extracted.url).hostname}</a>` : ''}
</div>
` : labels.includes('reply') ? `
<div class="p-in-reply-to h-cite">
<div class="p-name">${extracted.title || post.title.$t}</div>
${extracted.url ? `<a href="${extracted.url}" class="u-url">${new URL(extracted.url).hostname}</a>` : ''}
</div>
<div class="e-content p-name">${extracted.content}</div>
` : labels.includes('repost') ? `
<div class="p-repost-of h-cite">
<div class="p-name">${extracted.title || post.title.$t}</div>
${extracted.url ? `<a href="${extracted.url}" class="u-url">${new URL(extracted.url).hostname}</a>` : ''}
</div>
` : labels.includes('rsvp') ? `
<div class="p-rsvp">${extracted.title || post.title.$t}</div>
<div class="e-content">${extracted.content}</div>
` : `
<div class="e-content p-name">${extracted.content || post.title.$t}</div>
`}
${extracted.images.length > 0 ? `
<div class="post-image-container">
<img class="post-image u-photo" src="${extracted.images[0]}" alt="Post image">
${extracted.images.length > 1 ? `<div class="image-count">+${extracted.images.length - 1}</div>` : ''}
</div>
` : ''}
</div>
</a>
<div class="syndication-links">
${syndicationLinks.map(link => `
<a href="${link.url}" class="u-syndication" title="${link.domain}" target="_blank" rel="noopener noreferrer">
<img class="syndication-icon" src="${getSyndicationIcon(link.domain)}" alt="${link.domain}">
</a>
`).join('')}
</div>
`;
return card;
}
// Extract syndication links from content
function extractSyndicationLinks(content) {
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const syndicationElements = doc.querySelectorAll('a.u-syndication');
return Array.from(syndicationElements).map(a => ({
url: a.href,
domain: new URL(a.href).hostname
}));
}
// Get the appropriate syndication icon
function getSyndicationIcon(domain) {
const icons = {
'facebook.com': 'https://example.com/facebook-icon.png',
'twitter.com': 'https://example.com/twitter-icon.png',
'linkedin.com': 'https://example.com/linkedin-icon.png',
'mastodon.social': 'https://example.com/mastodon-icon.png',
'medium.com': 'https://example.com/medium-icon.png',
'reddit.com': 'https://example.com/reddit-icon.png'
};
return icons[domain] || 'https://example.com/default-icon.png';
}
// Filter posts based on selected type
function filterPosts(posts, filter) {
if (filter === 'all') return posts;
return posts.filter(post => {
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
return labels.includes(filter) || (filter === 'post' && !labels.some(label => ['like', 'reply', 'bookmark', 'repost', 'rsvp'].includes(label)));
});
}
// Load and display posts
async function loadPosts(page) {
const newPosts = await fetchPosts(page);
allPosts = allPosts.concat(newPosts);
const filteredPosts = filterPosts(allPosts, currentFilter);
feedElement.innerHTML = '';
displayedPostsCount = 0;
filteredPosts.forEach(post => {
const labels = post.category ? post.category.map(cat => cat.term.toLowerCase()) : [];
if (labels.includes('social') || labels.includes('syndicated')) {
const card = createPostCard(post);
feedElement.appendChild(card);
displayedPostsCount++;
}
});
loadMoreButton.style.display = displayedPostsCount >= 20 ? 'block' : 'none';
}
// Event Listeners
loadMoreButton.addEventListener('click', () => {
currentPage++;
loadPosts(currentPage);
});
document.querySelector('.filters').addEventListener('click', (event) => {
if (event.target.classList.contains('filter-btn')) {
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
currentFilter = event.target.dataset.filter;
currentPage = 1;
allPosts = [];
loadPosts(currentPage);
}
});
// Initial load
loadPosts(currentPage);
Limitations and What You Could Improve
Since I built this with my custom Blogger theme in mind, I’m not sure how well it will work on other themes without some tweaks. And while it’s functional, there’s always room for improvement! Here are a few things I did from my end:
Adding More Syndication Icons: Right now, only a few platforms are supported in the syndication links. It would be nice to add more to cover a broader range of social platforms.
Minimizing CSS and JavaScript: To make the code even lighter, you should minify the CSS and JavaScript files or only load necessary code based on the current page or view.
Optimizing Load Times: Loading lots of posts can slow down the page, so one improvement would be to implement lazy loading or caching.
How You Can Use It
Feel free to grab this code and experiment on your own Blogger site! This code might not be plug-and-play for every theme, but with a few tweaks, it should work for most. It’s a great way to add an interactive feed to your site, especially if you’re interested in IndieWeb features or just want to bring a more social, dynamic feel to Blogger.
Let me know if you try it out, or if you have any ideas on improving it further or any problem! And checkout some blogger templates I made that are compatible for a social feed page.