fix: switch blog pagination to client-side

Astro 6 getStaticPaths+paginate has issues with non-dynamic pages.
Client-side pagination works fine for small blog volumes.
This commit is contained in:
Kunthawat Greethong
2026-07-01 14:39:57 +07:00
parent a7a2724c09
commit fd6b70dbf2

View File

@@ -2,18 +2,14 @@
import PageShell from '../components/PageShell.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths({ paginate }) {
const posts = (await getCollection('blog', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
const POSTS_PER_PAGE = 12;
return paginate(posts, { pageSize: 12 });
}
const { page } = Astro.props;
const posts = (await getCollection('blog', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
// Collect all unique categories
const allCategories = [...new Set(page.data.map((p) => p.data.category))].sort();
const allCategories = [...new Set(posts.map((p) => p.data.category))].sort();
---
<PageShell
@@ -38,7 +34,7 @@ const allCategories = [...new Set(page.data.map((p) => p.data.category))].sort()
</div>
<div class="blog-grid" id="blog-grid">
{page.data.map((post) => {
{posts.map((post) => {
const fmt = new Intl.DateTimeFormat('th-TH', { day: '2-digit', month: 'short', year: '2-digit' }).format(post.data.pubDate);
return (
<a class="blog-card liquid-glass liquidGlass-wrapper" href={`/blog/${post.id}/`} data-cat={post.data.category}>
@@ -59,32 +55,83 @@ const allCategories = [...new Set(page.data.map((p) => p.data.category))].sort()
<p class="blog-empty" id="blog-empty" hidden>ไม่พบบทความที่ตรงกับหมวดหมู่นี้</p>
<!-- Pagination -->
{page.lastPage > 1 && (
<nav class="blog-pagination" aria-label="หน้าบทความ">
{page.url.prev ? (
<a class="pagination-btn" href={page.url.prev} aria-label="หน้าก่อนหน้า">
← ก่อนหน้า
</a>
) : (
<span class="pagination-btn pagination-btn--disabled">← ก่อนหน้า</span>
)}
<span class="pagination-info">
หน้า {page.currentPage} จาก {page.lastPage}
</span>
{page.url.next ? (
<a class="pagination-btn" href={page.url.next} aria-label="หน้าถัดไป">
ถัดไป →
</a>
) : (
<span class="pagination-btn pagination-btn--disabled">ถัดไป →</span>
)}
</nav>
)}
<nav class="blog-pagination" id="blog-pagination" aria-label="หน้าบทความ" hidden>
<button class="pagination-btn" id="prev-btn" disabled>← ก่อนหน้า</button>
<span class="pagination-info" id="page-info"></span>
<button class="pagination-btn" id="next-btn">ถัดไป →</button>
</nav>
</section>
</PageShell>
<script is:inline define:vars={{ POSTS_PER_PAGE }}>
const grid = document.getElementById('blog-grid');
const tagsEl = document.getElementById('blog-tags');
const empty = document.getElementById('blog-empty');
const pagination = document.getElementById('blog-pagination');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const pageInfo = document.getElementById('page-info');
const allCards = Array.from(grid?.querySelectorAll('.blog-card') || []);
const tagBtns = tagsEl?.querySelectorAll('.blog-tag');
let currentPage = 1;
let activeCategory = 'all';
function getVisibleCards() {
return allCards.filter(card =>
activeCategory === 'all' || card.dataset.cat === activeCategory
);
}
function updateDisplay() {
const visible = getVisibleCards();
const totalPages = Math.ceil(visible.length / POSTS_PER_PAGE);
const start = (currentPage - 1) * POSTS_PER_PAGE;
const end = start + POSTS_PER_PAGE;
// Hide all cards
allCards.forEach(card => card.style.display = 'none');
// Show current page cards
visible.slice(start, end).forEach(card => card.style.display = '');
// Update pagination
empty.hidden = visible.length > 0;
pagination.hidden = totalPages <= 1;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
pageInfo.textContent = totalPages > 0
? `หน้า ${currentPage} จาก ${totalPages}`
: '';
}
// Category filter
if (tagBtns) {
tagBtns.forEach((btn) => {
btn.addEventListener('click', () => {
activeCategory = btn.dataset.cat;
currentPage = 1;
tagBtns.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
updateDisplay();
});
});
}
// Pagination buttons
prevBtn?.addEventListener('click', () => {
if (currentPage > 1) { currentPage--; updateDisplay(); }
});
nextBtn?.addEventListener('click', () => {
const totalPages = Math.ceil(getVisibleCards().length / POSTS_PER_PAGE);
if (currentPage < totalPages) { currentPage++; updateDisplay(); }
});
// Initial render
updateDisplay();
</script>
<style>
.blog-section::before { display: none; }
@@ -194,17 +241,16 @@ const allCategories = [...new Set(page.data.map((p) => p.data.category))].sort()
color: var(--ink);
font-size: 0.88rem;
font-weight: 700;
text-decoration: none;
cursor: pointer;
transition: all .2s var(--ease);
}
.pagination-btn:hover {
.pagination-btn:hover:not(:disabled) {
background: var(--yellow);
border-color: var(--yellow);
}
.pagination-btn--disabled {
.pagination-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.pagination-info {
color: var(--muted);
@@ -233,31 +279,3 @@ const allCategories = [...new Set(page.data.map((p) => p.data.category))].sort()
}
}
</style>
<script>
const tagsEl = document.getElementById('blog-tags');
const grid = document.getElementById('blog-grid');
const empty = document.getElementById('blog-empty');
const cards = grid?.querySelectorAll('.blog-card');
const tagBtns = tagsEl?.querySelectorAll('.blog-tag');
if (tagBtns && cards) {
tagBtns.forEach((btn) => {
btn.addEventListener('click', () => {
const cat = btn.dataset.cat;
tagBtns.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
let visible = 0;
cards.forEach((card) => {
if (cat === 'all' || card.dataset.cat === cat) {
card.style.display = '';
visible++;
} else {
card.style.display = 'none';
}
});
empty.hidden = visible > 0;
});
});
}
</script>