Skip to content

Solving VitePress Blog Pagination: My Pitfalls and Implementation Journey

Why did I choose VitePress to build my personal website and blog? It wasn't a spur-of-the-moment decision; I was drawn to several key aspects:

  1. Familiar and User-Friendly: I had previously used VitePress for technical documentation and was familiar with its configuration, Markdown syntax, Vue component support, and its incredibly fast HMR (Hot Module Replacement). Using a familiar tool naturally saves time and effort.
  2. Blog Posts and Tutorials: My site isn't just for scattered blog posts; I also wanted to systematically include tutorial series (like Python basics, how to use FFmpeg). VitePress's file-based routing and flexible sidebar are inherently suited for this "documentation + blog" hybrid model, offering more flexibility than some pure blog systems.
  3. Fast Deployment and Performance: VitePress is simple to install and deploy. Built on Vite, its build speed is impressive, and the resulting static site performance is excellent – exactly what I needed.

However, as I wrote more blog posts, a problem emerged. VitePress's default sidebar seems more designed for well-structured documentation. Faced with a large number of chronologically ordered blog posts, it struggles – there's no pagination. With many articles, the sidebar becomes endlessly long, making it tedious for readers. This had to change! So, I explored whether I could use VitePress's own features to create a dynamically loaded, time-sorted, paginated list of the latest articles on the homepage (index.md).

Seeking AI Help: Almost Led Astray

These days, when facing a technical challenge, who doesn't think of asking AI first? I figured VitePress 1.6.3 was relatively new, so I needed an AI with internet access. I tried Grok-3, which claims to fetch the latest information, hoping it could directly provide usable code or reliable ideas.

The result? The communication with Grok-3 was a "disaster"! I clearly explained my requirements, directory structure, and VitePress version, but its solutions were consistently flawed:

  • It confused APIs between VitePress 0.x and 1.x.
  • It suggested reinstalling [email protected], using a custom theme approach, or even directly modifying JS files in the node_modules/vitepress default theme to resolve plugin startup issues.
  • The provided code snippets often had import errors, incorrect API calls, or illogical flow, consistently causing startup failures. When it did start, the functionality wasn't implemented, leaving the homepage blank.
  • After numerous back-and-forth attempts with minimal improvement, I hit its usage frequency limit, requiring me to wait an hour – truly frustrating.

This ordeal made me realize that even if an AI claims internet access, its depth of understanding and accuracy regarding specific frameworks (especially those evolving rapidly) should be questioned. Over-reliance can lead to wasted effort on the wrong path.

Out of options, I tried Gemini 2.5 Pro exp. Surprisingly, communication with Gemini was much smoother! After describing my requirements, it quickly suggested a solution based on the VitePress 1.6.x core feature createContentLoader. We only encountered one minor issue related to theme resolution (@theme/index). I posted the error, and Gemini immediately pinpointed the problem (missing custom theme entry file) and provided the correct fix (create a theme/index.js inheriting the default theme).

This experience highlighted the stark difference in capability between different AI models when handling specific framework issues. Gemini's understanding of VitePress 1.x was clearly more accurate, and its solution aligned better with the framework's best practices.

Light at the End of the Tunnel: createContentLoader to the Rescue

The key to success was a "magic tool" built into VitePress 1.x – createContentLoader. It allows us to load and process Markdown files during the build phase, bundling the processed data for direct use by frontend Vue components.

Here's how it's done, step by step:

  1. Load Data (.vitepress/theme/posts.data.js): This is the core! Create a posts.data.js file using createContentLoader:

    javascript
    // .vitepress/theme/posts.data.js
    import { createContentLoader } from 'vitepress'
    import matter from 'gray-matter' // This library parses frontmatter and content
    
    // Implementation of createExcerpt function omitted here; it mainly extracts plain text summaries
    function createExcerpt(content, maxLength) {
        // ... Logic to remove Markdown/HTML tags and truncate ...
        // Simple example: Remove tags, take first maxLength characters
        const plainText = content.replace(/<[^>]*>/g, '').replace(/#+\s/g, '');
        return plainText.slice(0, maxLength) + (plainText.length > maxLength ? '...' : '');
    }
    
    
    export default createContentLoader('**/*.md', { // Match all Markdown files
      includeSrc: true, // Need source content to extract excerpts
      ignore: ['index.md', '**/node_modules/**', '.vitepress/**'], // Exclude non-article files
      transform(rawData) {
        return rawData
          // Filter out md files with incomplete frontmatter or non-target content
          .filter(({ frontmatter, src }) => frontmatter && frontmatter.date && frontmatter.title && src)
          .map(({ url, frontmatter, src }) => {
            // Use gray-matter to separate frontmatter and pure Markdown content
            const { content } = matter(src);
            return {
              title: frontmatter.title,
              url,
              // Ensure date is a Date object for sorting
              date: new Date(frontmatter.date),
              // Call our function to generate excerpt
              excerpt: createExcerpt(content, 200),
            }
          })
          // Sort by date descending, newest first
          .sort((a, b) => b.date.getTime() - a.date.getTime());
      }
    })
    • '**/*.md': Tells VitePress to scan all Markdown files in all directories.
    • ignore: Excludes files like the homepage, node_modules, .vitepress directory that shouldn't be treated as articles.
    • includeSrc: true: Crucial – must be true to get the raw Markdown content for generating excerpts.
    • The transform function is where the magic happens:
      • First filter to ensure articles have the required frontmatter fields: title and date.
      • Then map processes each qualifying file: use gray-matter to separate frontmatter and content, then call the createExcerpt function to generate a summary (you need to write this function, logic involves stripping Markdown/HTML tags and truncating to ~200 chars).
      • Finally returns an array of objects containing title, url, date, excerpt.
      • Don't forget sort – sort by date descending so the newest articles come first.
  2. Display Component (.vitepress/theme/components/LatestPosts.vue): Create a Vue component to consume the processed data and implement pagination:

    vue
    <script setup>
    import { ref, computed } from 'vue'
    // Import build-time generated data; ensure the path is correct
    import { data as allPosts } from '../posts.data.js'
    
    const postsPerPage = ref(10); // Number of posts per page
    const currentPage = ref(1); // Current page number
    
    // Calculate total pages
    const totalPages = computed(() => Math.ceil(allPosts.length / postsPerPage.value));
    
    // Calculate which posts to display on the current page
    const paginatedPosts = computed(() => {
      const start = (currentPage.value - 1) * postsPerPage.value;
      const end = start + postsPerPage.value;
      return allPosts.slice(start, end);
    });
    
    // Function to change page
    function changePage(newPage) {
      if (newPage >= 1 && newPage <= totalPages.value) {
        currentPage.value = newPage;
      }
    }
    
    // Format date for better display
    function formatDate(date) {
      if (!(date instanceof Date)) date = new Date(date);
      return date.toLocaleDateString(); // Or use a more detailed format
    }
    </script>
    
    <template>
      <div class="latest-posts">
        <ul>
          <li v-for="post in paginatedPosts" :key="post.url">
            <div class="post-item">
                <a :href="post.url" class="post-title">{{ post.title }}</a>
                <span class="post-date">{{ formatDate(post.date) }}</span>
                <p v-if="post.excerpt" class="post-excerpt">{{ post.excerpt }}</p>
            </div>
          </li>
        </ul>
    
        <!-- Pagination controls -->
        <div class="pagination">
          <button @click="changePage(currentPage - 1)" :disabled="currentPage === 1">
            Previous
          </button>
          <span>Page {{ currentPage }} / {{ totalPages }}</span>
          <button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages">
            Next
          </button>
        </div>
      </div>
    </template>
    
    <style scoped>
    /* Add some styles to make the list look better */
    .latest-posts ul { list-style: none; padding: 0; }
    .post-item { margin-bottom: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 1em; }
    .post-title { font-size: 1.2em; font-weight: bold; display: block; margin-bottom: 0.3em; }
    .post-date { font-size: 0.9em; color: #888; margin-bottom: 0.5em; display: block;}
    .post-excerpt { font-size: 1em; color: #555; line-height: 1.6; margin-top: 0.5em; }
    .pagination { margin-top: 2em; text-align: center; }
    .pagination button { margin: 0 0.5em; padding: 0.5em 1em; cursor: pointer; }
    .pagination button:disabled { cursor: not-allowed; opacity: 0.5; }
    .pagination span { margin: 0 1em; }
    </style>
    • Use import { data as allPosts } from '../posts.data.js' to import all processed article data.
    • Use Vue's ref (reactive variables) and computed (computed properties) to implement pagination logic; the page updates automatically when data changes.
  3. Use on Homepage (index.md): In your homepage Markdown file (index.md), use the component like a regular HTML tag:

    markdown
    ---
    layout: home # Ensure your layout supports Vue components; the default home layout usually does
    ---
    <script setup>
    // Import our newly created component
    import LatestPosts from './.vitepress/theme/components/LatestPosts.vue'
    </script>
    
    # My Blog Homepage
    
    Welcome to my site...
    
    ## Latest Posts
    
    <LatestPosts /> <!-- Place the component where you want the list to appear -->
    
    Other content you want to include...
  4. Fix Theme Error (.vitepress/theme/index.js): How to resolve that annoying @theme/index error? It's simple. Once you create the .vitepress/theme directory (even just for placing posts.data.js or components), VitePress requires a theme entry file. Just create a theme/index.js that simply inherits the default theme:

    javascript
    // .vitepress/theme/index.js
    import DefaultTheme from 'vitepress/theme'
    // Just export an object that spreads the default theme
    export default { ...DefaultTheme }

    This lets VitePress know how to load the theme, and your custom posts.data.js and components will work normally.

Review and Summary: Pitfalls and Key Takeaways

This process was quite enlightening. Here are some key lessons learned:

  • createContentLoader is the Key: For VitePress 1.x, this is the officially recommended, performant, and convenient method for processing content data at build time.
  • Frontmatter Standardization is Crucial: The processing logic in posts.data.js heavily relies on the title and date fields in the Markdown frontmatter. Therefore, when writing articles, these must be standardized, especially the date format, otherwise data processing fails and the feature breaks.
  • Watch File Paths: When importing other files in .vue or data.js files, ensure relative paths are correct to avoid "file not found" errors.
  • Trade-offs in Excerpt Generation: Using regex to strip tags and generate plain text excerpts is a simple and fast method, but it might not handle very complex Markdown perfectly. However, it's usually sufficient for quick previews.
  • Beware the Theme Entry "Pitfall": Whenever you modify the .vitepress/theme directory, even just by adding a file, remember to provide theme/index.js, even if it only inherits the default theme. Otherwise, startup fails.
  • AI is a Helper, Not a "Deity": AI can certainly assist, but it cannot fully replace your understanding of the framework. When encountering problems, still learn to consult official documentation and debug yourself. Also, choosing the right AI model is critical – Gemini was much more effective than Grok in this case.

Finally, I successfully added the desired paginated article list to my VitePress homepage, preserving the structured navigation of the documentation site while meeting the needs of a blog post stream. Goal achieved (this very blog site uses this implementation)!

Although the process had its twists, it deepened my understanding of VitePress and gave me firsthand experience with the performance differences of various AI tools in practice – definitely worth it!