fix: pagination scrambling and scroll + feat: excludePostTypes filter

- Fix duplicate x-for keys causing scrambled pagination numbers on
  interactions page (two '…' entries shared the same key)
- Fix scroll target in goToPage — was using dead closest('[x-show]')
  selector, now scrolls to #webmentions-list
- Add flex-wrap to pagination-links for mobile overflow
- Add excludePostTypes Eleventy filter to exclude post types from
  collections by detecting type from frontmatter properties
- Wire excludePostTypes into recent-posts section via sectionConfig
- Add error/stale data banner to changelog page
This commit is contained in:
Ricardo
2026-03-26 16:25:36 +01:00
parent a3cb1c1f55
commit 8af3cc329d
4 changed files with 39 additions and 8 deletions

View File

@@ -7,15 +7,17 @@
{% set sectionConfig = section.config or {} %}
{% set maxItems = sectionConfig.maxItems or 5 %}
{% set showSummary = sectionConfig.showSummary if sectionConfig.showSummary is defined else true %}
{% set excludeTypes = sectionConfig.excludeTypes or [] %}
{% set recentPosts = collections.posts | excludePostTypes(excludeTypes) | head(maxItems) %}
{% if collections.posts and collections.posts.length %}
{% if recentPosts and recentPosts.length %}
<section class="mb-8 sm:mb-12">
<h2 class="text-xl sm:text-2xl font-bold text-surface-900 dark:text-surface-100 mb-4 sm:mb-6">
{{ sectionConfig.title or "Recent Posts" }}
</h2>
<div class="space-y-4">
{% for post in collections.posts | head(maxItems) %}
{% for post in recentPosts %}
{# Detect post type from frontmatter properties #}
{% set likedUrl = post.data.likeOf or post.data.like_of %}
{% set bookmarkedUrl = post.data.bookmarkOf or post.data.bookmark_of %}

View File

@@ -59,6 +59,14 @@ withSidebar: false
<span class="ml-3 text-surface-600 dark:text-surface-400">Loading changelog...</span>
</div>
{# Error/stale data banner #}
<div x-show="apiError && !loading" x-cloak class="mb-6 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-amber-800 dark:text-amber-200 text-sm">
<span class="font-medium">Note:</span>
<span x-text="commits.length > 0 ? 'Showing cached data — GitHub API is temporarily unavailable.' : apiError"></span>
</p>
</div>
{# Commit list #}
<div x-show="!loading" x-cloak>
<template x-if="filteredCommits().length === 0">
@@ -129,6 +137,7 @@ function changelogApp() {
viewMode: 'repo',
loading: true,
loadingMore: false,
apiError: null,
commits: [],
categories: {},
commitCategories: {},
@@ -239,14 +248,16 @@ function changelogApp() {
async fetchChangelog(days) {
try {
const response = await fetch('/githubapi/api/changelog?days=' + days);
if (!response.ok) throw new Error('Failed to fetch');
if (!response.ok) throw new Error('Failed to fetch changelog (HTTP ' + response.status + ')');
const data = await response.json();
this.commits = data.commits || [];
this.categories = data.categories || {};
this.commitCategories = data.commitCategories || {};
this.currentDays = data.days;
this.apiError = data.error || null;
} catch (err) {
console.error('Changelog error:', err);
this.apiError = err.message || 'Failed to load changelog';
} finally {
this.loading = false;
this.loadingMore = false;

View File

@@ -655,6 +655,25 @@ export default function (eleventyConfig) {
return array.slice(0, n);
});
// Exclude post types from a collection by detecting type from frontmatter properties
// Usage: collections.posts | excludePostTypes(["reply", "like"])
// Supported types: reply, like, bookmark, repost, photo, article, note
eleventyConfig.addFilter("excludePostTypes", (posts, excludeTypes) => {
if (!Array.isArray(posts) || !Array.isArray(excludeTypes) || !excludeTypes.length) return posts;
return posts.filter((post) => {
const d = post.data || {};
let type;
if (d.inReplyTo || d.in_reply_to) type = "reply";
else if (d.likeOf || d.like_of) type = "like";
else if (d.bookmarkOf || d.bookmark_of) type = "bookmark";
else if (d.repostOf || d.repost_of) type = "repost";
else if (d.photo && d.photo.length) type = "photo";
else if (d.title) type = "article";
else type = "note";
return !excludeTypes.includes(type);
});
});
// Slugify filter
eleventyConfig.addFilter("slugify", (str) => {
if (!str) return "";

View File

@@ -216,7 +216,7 @@ permalink: /interactions/
</div>
{# Webmentions list #}
<div x-show="!notConfigured && (!loading || webmentions.length)" class="space-y-4">
<div id="webmentions-list" x-show="!notConfigured && (!loading || webmentions.length)" class="space-y-4">
<template x-for="wm in paginatedWebmentions" :key="wm['wm-id']">
<div class="p-4 bg-surface-50 dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-sm">
<div class="flex gap-3">
@@ -297,7 +297,7 @@ permalink: /interactions/
Page <span x-text="currentPage"></span> of <span x-text="totalPages"></span>
<span class="text-surface-600 dark:text-surface-400 ml-1">(<span x-text="filteredWebmentions.length"></span> total)</span>
</div>
<div class="pagination-links">
<div class="pagination-links flex-wrap">
<button
@click="goToPage(currentPage - 1)"
:disabled="currentPage <= 1"
@@ -307,7 +307,7 @@ permalink: /interactions/
Previous
</button>
<template x-for="p in pageNumbers" :key="p">
<template x-for="(p, idx) in pageNumbers" :key="'pg-' + idx">
<button
@click="typeof p === 'number' && goToPage(p)"
:disabled="p === '…'"
@@ -413,8 +413,7 @@ function interactionsApp() {
goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
// Scroll to top of inbound tab
this.$el.closest('[x-show]')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
document.getElementById('webmentions-list')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
},
async init() {