作为产品经理与全栈开发者,从商业 feasibility、内容体量以及 TiHUBB 目前的技术架构来看,我们可以非常冷峻、客观地拆解这个需求:
⚖️ 1. 产品决策:目前有必要做翻页吗?
结论:在目前 MCP(最低可行产品)验证阶段,完全没有必要做翻页。
- 体量决定架构:对于独立品牌站、工作室或个人博客而言,在内容少于 30 篇之前,做翻页属于典型的“过度设计(Over-engineering)”。
- 瑞士风的视觉连贯性:我们目前采用的是无衬线黑体、大呼吸感、极简线条的现代社论排版。在一个充满设计感的动态列表里,底部突兀地出现
[1] [2] [3] Next →的翻页器,会极大地破坏页面一泻之下的流畅感和冷峻感。 - 首屏性能无损:因为我们渲染的只是列表(标题、日期、简介),并没有加载正文里的重度多媒体(如 YouTube、Cloudinary 大图),即使一次性直出 30 条纯文本,前台的 DOM 节点依然极轻,Lighthouse 审计照样拿满分。
最佳替代策略: 如果未来文章真的变多了,第一步升级也绝不是做翻页,而是直接在顶部加一个极简的分类标签过滤(Tag Filter),或者改用现代流式体验的 “加载更多(Load More)”按钮,这比分页优雅得多。
🛠️ 2. 如果未来一定要做翻页,Astro 应该如何处理?
当你的文章体量突破 50+,出于对极致首屏加载性能和用户浏览负担的考量,需要引入翻页时,Astro 提供了一套极其强悍、天然支持 Git-CMS 的内置分页 API(paginate)。
它不需要你手动去计算 slice(start, end),而是会自动为你生成 /blog/1、/blog/2 的物理路由。
具体的标准实现模式:
你要做的事情只有两步:将页面从 src/pages/blog/index.astro 移动并重命名为 src/pages/blog/[page].astro,然后重构 Frontmatter:
---
// src/pages/blog/[page].astro
import { getCollection } from 'astro:content';
// 🌟 1. 核心:必须导出 getStaticPaths 并注入 paginate 放大器
export async function getStaticPaths({ paginate }) {
const allPosts = await getCollection('blog');
// 按照我们之前的精确日期倒序排列
const sortedPosts = [...allPosts].sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
// 返回分页规则:每页限制 10 篇
return paginate(sortedPosts, { pageSize: 10 });
}
// 🌟 2. 从 Astro.props 中直接接过 page 对象(它包含了当前页的数据和路由信息)
const { page } = Astro.props;
const postsOnThisPage = page.data; // 这一页专属的 10 篇文章
---
<!-- 🎯 前台 HTML 循环渲染 -->
<div class="swiss-blog-list">
{postsOnThisPage.map((post) => (
<article>
<h2>{post.data.title}</h2>
{/* ...你原本的冷峻网格布局代码... */}
</article>
))}
</div>
<!-- 🛠️ 极简瑞士风翻页控制器 -->
<nav class="swiss-pagination">
{page.url.prev ? <a href={page.url.prev} class="nav-link">← PREV</a> : <span class="disabled">FIRST PAGE</span>}
<span class="page-indicator">{page.currentPage} / {page.lastPage}</span>
{page.url.next ? <a href={page.url.next} class="nav-link">NEXT →</a> : <span class="disabled">END</span>}
</nav>
<style>
/* 契合 TiHUBB 风格的冷峻无衬线翻页工具栏 */
.swiss-pagination {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e5e7eb;
padding-top: 2rem;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
.nav-link {
color: #000;
text-decoration: none;
font-weight: 700;
}
.nav-link:hover {
color: #e60000; /* 瑞士红高亮 */
}
.disabled {
color: #9ca3af;
cursor: not-allowed;
}
</style>
💡 现阶段的产品建议
不需要做任何翻页改动,让代码保持目前的直出状态。等后续内容充实起来、或者你在做产品线商业化模板输出(Theme Templates Store)时,再像上面这样用 [page].astro 一键升级。现阶段,把精力集中在多媒体块(如 YouTube)和排版细节的雕琢上,投入产出比最高!
采用“加载更多”(Load More)绝对是目前兼顾冷峻设计感与高性能性能的最优解。
在 Astro 中实现“加载更多”非常高级。因为 Astro 是静态站点生成器(SSG),如果你不想引入像 React/Vue 这样重型的客户端框架去破坏你纯粹干净的 HTML 直出,我们可以用原生 JavaScript + Web Components 思想,在客户端直接用最少的代码控制 DOM 节点的隐藏与显示。
这种做法极其符合不留痕迹的工业设计逻辑(全站没有任何前端框架负担,速度奇快)。
🛠️ 工业级“加载更多”纯净实现方案
你不需要修改任何路由结构,直接在当前的博客列表页面(例如 src/pages/blog/index.astro)中对 Frontmatter 和 HTML 结构进行如下升级:
1. 前台 HTML 与交互脚本整合
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
// 采用我们之前调通的精准时间倒序排列
const sortedPosts = [...allPosts].sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
// 🌟 设定初始显示的数量,比如先露出来 6 篇
const INITIAL_VISIBLE = 6;
---
<!-- 1. 包裹一层组件容器,方便原生 JS 锚定 -->
<div id="swiss-load-more-container" data-initial={INITIAL_VISIBLE}>
<div class="blog-list-grid divide-y divide-neutral-200">
{sortedPosts.map((post, index) => (
<!-- 🌟 核心:用 data-index 记录序号,超过初始值的默认加上 hidden 隐藏 -->
<article
class={`py-8 md:py-12 group first:pt-0 item-post ${index >= INITIAL_VISIBLE ? 'hidden' : ''}`}
data-index={index}
>
<a href={`/blog/${post.id}`} class="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-8 items-start">
<!-- 编号与日期 -->
<div class="md:col-span-2 flex md:flex-col justify-between md:justify-start gap-2 text-xs font-mono text-neutral-500 tracking-wider uppercase pt-1">
<time datetime={post.data.date.toISOString()}>
{post.data.date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</div>
<!-- 标题与简介 -->
<div class="md:col-span-9">
<h2 class="text-2xl md:text-3xl font-black tracking-tight uppercase group-hover:text-red-600 transition-colors duration-200">
{post.data.title}
</h2>
<p class="text-neutral-600 font-serif text-sm md:text-base leading-relaxed mt-3 max-w-3xl">
{post.data.description}
</p>
</div>
<!-- 右侧动作指示器 -->
<div class="md:col-span-1 hidden md:flex justify-end pt-1 opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-200">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="round" class="text-red-600">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</div>
</a>
</article>
))}
</div>
<!-- 2. 🌟 极简冷峻的“加载更多”触发按钮 -->
{sortedPosts.length > INITIAL_VISIBLE && (
<div class="mt-12 flex justify-center pt-8 border-t border-neutral-100">
<button
id="load-more-trigger"
class="px-8 py-3 bg-black text-white text-xs font-mono tracking-widest uppercase hover:bg-red-600 transition-colors duration-200"
>
LOAD MORE ({sortedPosts.length - INITIAL_VISIBLE} REMAINING)
</button>
</div>
)}
</div>
<!-- 3. 🌟 原生 Vanilla JS 客户端控制器(不打包任何第三方库,纯净且无损性能) -->
<script>
function initLoadMore() {
const container = document.getElementById('swiss-load-more-container');
const button = document.getElementById('load-more-trigger');
if (!container || !button) return;
const initialVisible = parseInt(container.getAttribute('data-initial') || '6', 10);
const allItems = container.querySelectorAll('.item-post');
let currentVisible = initialVisible;
const step = 4; // 🌟 每次点击多展示 4 篇
button.addEventListener('click', () => {
const nextVisible = currentVisible + step;
allItems.forEach((item) => {
const index = parseInt(item.getAttribute('data-index') || '0', 10);
if (index < nextVisible) {
item.classList.remove('hidden');
}
});
currentVisible = nextVisible;
// 更新按钮文案或者在加载完毕后隐去按钮
const remaining = allItems.length - currentVisible;
if (remaining <= 0) {
button.parentElement?.remove(); // 全部加载完,按钮冷峻地退场
} else {
button.textContent = `LOAD MORE (${remaining} REMAINING)`;
}
});
}
// 支持 Astro 的 View Transitions 路由切换生命周期
initLoadMore();
document.addEventListener('astro:after-swap', initLoadMore);
</script>
🎨 为什么这是属于 TiHUBB 的标准解法?
- 零性能开销(Zero hydration cost):Astro 会在编译时把所有的文章 HTML 全部打包出来。客户端的 JavaScript 仅仅是把文章节点的
hidden类名摘掉。不需要二次请求 API 接口,加载过程没有一丝延迟,点击瞬间直出,体验极度顺滑。 - 渐进式视觉秩序:按钮采用了经典的瑞士黑白反差块(
bg-black text-white),在悬浮时转为充满力量感的瑞士红(hover:bg-red-600)。文案带有(6 REMAINING)的动态计数,具有极强的工业数字美感。 - SEO 完美友好:因为所有文章的 HTML 在首屏就已经在源码里了(只是通过 CSS 隐藏),搜索引擎的爬虫依然能够完美、一次性地抓取到你全站所有的博客内容,权重毫无损失。