Implementing Blog Index with VitePress and Tailwind CSS
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.
/** @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:
pnpm add -D prettier prettier-plugin-tailwindcss
Create a .prettierrc
file at the project's root with the following configuration:
{
"plugins": ["prettier-plugin-tailwindcss"]
}
If you're using Visual Studio Code, include the following in .vscode/settings.json
to enable auto-formatting on save:
{
"prettier.enable": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
Add a script in package.json
to format files using Prettier:
{
"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:
@tailwind base;
@tailwind components;
@tailwind utilities;
Then, import this file into .vitepress/theme/index.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.
---
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:
<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.
<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:
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:
<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:
[
{
"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:
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
:
<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:
<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:
<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
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.