Implementing Blog Index with VitePress and Tailwind CSS

- Lire en français
Resources: garabit

Before diving into the core task, we must set up Tailwind CSS. This step, while straightforward, is essential for styling our blog effectively.

Tailwind CSS Configuration

We need to direct Tailwind CSS to the location of our files. In the tailwind.config.cjs file at the project root, utilize the content key to specify which files to analyze to generate the necessary CSS classes.

js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.md', './.vitepress/theme/**/*.{vue,ts}'],
  // ...
}

We instruct Tailwind CSS to scan both Markdown files and all Vue and TypeScript files in the theme directory.

Note

Monitoring Markdown files is vital since HTML can exist within them, and components can also be included.

Integrating Prettier with Tailwind CSS Plugin

Additionally, we will configure Prettier for code formatting and implement the Prettier Tailwind CSS plugin to automatically sort classes.

To install Prettier and the plugin, execute:

sh
pnpm add -D prettier prettier-plugin-tailwindcss

Create a .prettierrc file at the project's root with the following configuration:

json
{
  "plugins": ["prettier-plugin-tailwindcss"]
}

If you're using Visual Studio Code, include the following in .vscode/settings.json to enable auto-formatting on save:

json
{
  "prettier.enable": true,
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

Add a script in package.json to format files using Prettier:

json
{
  "scripts": {
    "format": "prettier --write ."
  }
}

Connecting Tailwind CSS to the Project

Finally, we need to link our project with the CSS generated by Tailwind CSS. In .vitepress/theme/styles, create an app.css file and append the Tailwind CSS directives:

css
@tailwind base;
@tailwind components;
@tailwind utilities;

Then, import this file into .vitepress/theme/index.ts:

ts
import { Theme } from 'vitepress'
import Layout from './Layout.vue'

import './styles/app.css'

export default {
  Layout,
} satisfies Theme

Importing a CSS file directly within a TypeScript file poses no issues as Vite seamlessly handles it.

Now, we're prepared to start styling our blog!

Developing the Blog Page

We require a page to display all our blog articles. As discussed in earlier articles, VitePress treats Markdown files in the src directory as pages. Thus, we'll create a blog.md file in the src directory. This page will map to the /blog route.

md
---
layout: blog
title: Blog
description: The latest articles, tutorials, and resources on web development, programming, and more.
---

The body remains empty to allow the component to manage the page structure. This component will include:

  • A comprehensive list of articles
  • A navigation button to return to the homepage

Tip

Determine content placement by considering theme-neutral content. For instance, is the article list part of the theme or the content? A theme may display this list in various formats—column, grid, or carousel—which the theme dictates. Content must remain theme-agnostic. There is some exception to this rule, especially for inlined content.

Routing the Blog Page to the Component

Within .vitepress/theme/pages, create the Blog/BlogIndex.vue component:

vue
<script lang="ts" setup>

</script>

<template>
  <section>
    <h1>Blog</h1>
  </section>
</template>

This component structures the page. It employs the correlated Markdown file to present the title and articles.

In Layout.vue, link the layout to the component by extracting the layout key from the current file's frontmatter for appropriate component display.

vue
<script lang="ts" setup>
const { frontmatter } = useData()
</script>

<template>
  <div>
    <main>
      <BlogIndex v-if="frontmatter.layout === 'blog'" />
    </main>
  </div>
</template>

Now, navigating to the /blog route displays the BlogIndex component as VitePress loads the blog.md file, exposing its frontmatter to Layout.vue through the useData function.

Loading All Articles

Now comes the exciting part! We'll load all articles from the src/blog directory. Employing VitePress data loaders, this can be achieved with minimal code and no runtime burden. ✨

Note

Ensure the presence of some Markdown files in the src/blog directory. You can copy them from the GitHub repository.

In a data directory within .vitepress, create the blog.data.ts file:

ts
import type { ContentData } from 'vitepress'
import { createContentLoader } from 'vitepress'

declare const data: ContentData[]
export { data }

export default createContentLoader('blog/*.md')

Note

Ensure the extension .data.ts is used; it's crucial for functionality.

What's happening here? VitePress anticipates local Markdown file loading, providing a helper function for the task. Simply specify the directory.

The default type may be incorrect due to server transformation before data access. Export a custom type via the data variable for correct imports.

Load this module in the BlogIndex.vue component and render it to view the result:

vue
<script lang="ts" setup>
import { data } from '../../../data/blog.data'
</script>

<template>
  <section>
    <h1>Blog</h1>

    {{ data }}
  </section>
</template>

Data output is an array of objects, each representing a Markdown file. You can view each file's frontmatter and URL:

json
[
  {
    "frontmatter": {
      "title": "Getting Started with Vue 3 and Vite",
      "description": "Learn how to set up a new Vue 3 project with Vite, the fast build tool for modern web development. Explore the basics of Vue 3, including the Composition API, reactivity, components, and more.",
      "date": "2024-09-14T00:00:00.000Z"
    },
    "url": "/blog/getting-started-with-vue3-and-vite.html"
  },
  {
    "frontmatter": {
      "title": "Getting Started with Laravel",
      "description": "Learn how to set up a new Laravel project and build a simple web application. Explore the basics of Laravel, including routing, views, controllers, and more.",
      "date": "2024-10-14T00:00:00.000Z"
    },
    "url": "/blog/getting-started-wth-laravel.html"
  }
]

How does it function? VitePress uses a Vite plugin to detect imports from a .data.ts file. Upon detection, data loading occurs, returning only the results to the client. Consequently, data is transformed during build time, offering immediate browser availability, akin to virtual modules in Vite, yet more tailored to VitePress' needs.

Article Sorting

Currently, articles appear sorted by name, mimicking file directory ordering. Prioritize sorting them by date, showcasing the latest articles first.

Optimization is achievable at build time, obviating runtime computation during hydration.

In blog.data.ts, sort data by date with a custom transformer:

ts
import type { ContentData } from 'vitepress'
import { createContentLoader } from 'vitepress'

declare const data: ContentData[]
export { data }

export default createContentLoader('blog/*.md', {
  transform: (data) => {
    return data.sort((a, b) => {
      return (
        new Date(b.frontmatter.date).getTime()
          - new Date(a.frontmatter.date).getTime()
      )
    })
  },
})

The data now appears sorted by date, with the most recent article leading.

Note

VitePress caches data output. To witness updates, restart the development server.

Displaying the Articles

With articles ready for loading and sorting, our focus shifts to styling! 🎨

Begin by creating a BlogCard.vue component in .vitepress/theme/components:

vue
<script lang="ts" setup>
import { ContentData } from 'vitepress'

defineProps<{
  post: ContentData
}>()
</script>

<template>
  <article
    class="group relative border-4 border-black bg-white p-12 shadow-[8px_8px_0_black] transition duration-150 ease-linear space-y-6 hover:shadow-[12px_12px_0_black] hover:-translate-x-1 hover:-translate-y-1"
  >
    <h2 class="text-4xl text-black font-medium">
      <a :href="post.url">
        {{ post.frontmatter.title }}
        <span class="absolute inset-0" />
      </a>
    </h2>

    <p class="text-xl text-black">
      {{ post.frontmatter.description }}
    </p>
  </article>
</template>

Enhance the BlogIndex.vue component to display articles with a loop:

vue
<script lang="ts" setup>
import { data } from '../../../data/blog.data'

const { frontmatter } = useData()
</script>

<template>
  <section class="space-y-16">
    <h1 class="text-6xl font-semibold">
      {{ frontmatter.title }}
    </h1>

    <div class="space-y-10">
      <BlogCard v-for="post in data" :key="post.url" :post="post" />
    </div>

    <div class="flex justify-center">
      <a
        href="/"
        class="border-4 border-black bg-[#FFEB00] px-8 py-4 shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:bg-[#fff90d] hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]"
      >Back to Home</a>
    </div>
  </section>
</template>

Finally, update Layout.vue with styles to enhance the blog's visual appeal:

vue
<script lang="ts" setup>
const { frontmatter } = useData()
</script>

<template>
  <div class="min-h-screen bg-[#FC88FF] pt-16">
    <main class="mx-auto max-w-screen-md">
      <BlogIndex v-if="frontmatter.layout === 'blog'" />
    </main>
  </div>
</template>

Conclusion

Blog Articles
Blog Articles

VitePress simplifies the creation of index pages! The data loader enables article loading and sorting with minimal code, eliminating runtime overhead. This powerful feature allows us to focus on developing content and designing our blog.