← 返回日志

博客文章列表有必要做翻页吗?.

作为产品经理与全栈开发者,从商业 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 的标准解法?

  1. 零性能开销(Zero hydration cost):Astro 会在编译时把所有的文章 HTML 全部打包出来。客户端的 JavaScript 仅仅是把文章节点的 hidden 类名摘掉。不需要二次请求 API 接口,加载过程没有一丝延迟,点击瞬间直出,体验极度顺滑。
  2. 渐进式视觉秩序:按钮采用了经典的瑞士黑白反差块(bg-black text-white),在悬浮时转为充满力量感的瑞士红(hover:bg-red-600)。文案带有 (6 REMAINING) 的动态计数,具有极强的工业数字美感。
  3. SEO 完美友好:因为所有文章的 HTML 在首屏就已经在源码里了(只是通过 CSS 隐藏),搜索引擎的爬虫依然能够完美、一次性地抓取到你全站所有的博客内容,权重毫无损失。

相关文章 / 延续阅读 →