Create components in Vitepress using Tailwind

Jeremy
5 min readMay 30, 2023
Vite + Vuejs + Tailwind = A beautiful blog

In the previous article we saw how to create a blog using Vitepress and we used HTML in our markdown to display a card link to our article on our landing page. But this solution forces us to copy/paste the code for each new article. We’ll see how to create a better design for our card and how to load them automatically.

First we’ll create a component to extract the HTML from our index.mdand we’ll use Tailwind to create a professional look for our design.

Install Tailwind and start the initializing process:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

The others dependencies are used to integrate Tailwind to the Vite pipeline.

In tailwind.config.js add the following content:

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./blog/.vitepress/theme/components/*.vue",
"./blog/**/*.md",
],
theme: {
extend: {},
},
plugins: [],
}

This tell Tailwind where to look when searching for its classes in the components we will create and in our markdown files.

Next we create the file: blog/.vitepress/theme/css/custom.css is used for our CSS. We need to include Tailwind in it:

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

Creating a component

We want to create a card style component on our landing page to list our articles. This card should contain an thumbnail, a title, a small description and our date of publication.

Create the file blog/.vitepress/theme/components/ArticleCard.vue where our components will reside.

<template>
<a :href="href" class="flex flex-shrink-0 xl:flex-grow-0 w-full xl:w-[38rem] p-3 border-2 rounded-2xl hover:border-sky-500">
<div class="">
<div class="flex w-full">
<div class="flex flex-shrink-0">
<img class="rounded-2xl w-24 h-24 sm:w-48 sm:h-48 border-2" :src="image" :alt="title" />
</div>
<div class="flex flex-col ml-4">
<h3 class="w-full text-xl font-bold">{{ title }}</h3>
<small>{{ date }}</small>
<p>{{ excerpt }}</p>
</div>
</div>
</div>
</a>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
excerpt: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
date: {
type: String,
required: true,
},
href: {
type: String,
required: true,
},
},
};
</script>

We need to register the component in blog/.vitepress/theme/index.js :

import DefaultTheme from 'vitepress/theme-without-fonts'
import './css/custom.css'
import ArticleCard from "./components/ArticleCard.vue";

export default {
extends: DefaultTheme,
enhanceApp(ctx) {
ctx.app.component('ArticleCard', ArticleCard);
}
}

And now use it on our landing page index.md.

---
# https://vitepress.dev/reference/default-theme-home-page
layout: home

hero:
name: "My Awesome Blog"
text: "Talking about the future !"
tagline: My great project tagline
---

<ArticleCard
title="My new Article"
excerpt="Est commodi iusto et alias deleniti sed voluptatibus tempora est reprehenderit autem..."
image="/thumbnail-01.png"
href="/articles/article-1"
date="2023-05-29"
/>
Our landing page is looking good

We’ve also added an image in the blog/public/ folder to have a better look.

We have a problem, the card is aligned to the left. It might not be a problem on small screen but on a large one it will immediately appear.
We have two solutions to fix the problem:

  1. Add HTML/CSS around our ArticleCard in index.md to place our card (easy solution).
  2. Create a component that will contain our ArticleCard and load automatically our articles to our landing page (cleaner solution).

Let’s do both.

1. Integrating a Vue component in our Markdown

In index.md let’s add other articles and center them on the page. We’ll surround our components with a container centered on the page that’ll display 2 articles per row on large screens.


<div class="container mx-auto flex flex-wrap justify-center gap-x-4 gap-y-4">

<ArticleCard
title="My new Article 1"
excerpt="Est commodi iusto et alias deleniti sed voluptatibus tempora est reprehenderit autem..."
image="/thumbnail-01.png"
href="/articles/article-1"
date="2023-05-29"
/>

<ArticleCard
title="My new Article 2"
excerpt="Est commodi iusto et alias deleniti sed voluptatibus tempora est reprehenderit autem..."
image="/thumbnail-01.png"
href="/articles/article-1"
date="2023-05-29"
/>

<ArticleCard
title="My new Article 3"
excerpt="Est commodi iusto et alias deleniti sed voluptatibus tempora est reprehenderit autem..."
image="/thumbnail-01.png"
href="/articles/article-1"
date="2023-05-29"
/>

</div>

And the result:

See the nice tiled cards on our landing page

2. Automatically load articles on the landing page

We’ll create a data loader in blog/.vitepress/theme/index.data.js . It will load each article in articles/ and return the Url, title, description, … for each article.

import { createContentLoader } from 'vitepress'

export default createContentLoader('articles/*.md', {
excerpt: true,
transform(raw) {
return raw
.map(({ url, frontmatter, excerpt }) => ({
title: frontmatter.title,
url,
excerpt: truncateText(frontmatter.description, 100),
date: formatDate(frontmatter.date),
image: getImagePath(url)
}))
.sort((a, b) => b.date.time - a.date.time)
}
})

function truncateText(text, length) {
if (text.length > length) {
return text.substring(0, length) + "...";
}
return text;
}

function formatDate(raw) {
const date = new Date(raw)
date.setUTCHours(12)
return {
time: +date,
string: date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
}

function getImagePath(url) {
const filename = url.split('/').slice(-1)[0].split('.')[0]
return filename + '.png'
}

Then let’s create our CardContainer.vue component. It will iterate on the article list and create the article card for each of them.

<script setup>
import ArticleCard from "./ArticleCard.vue";
import { data as articles } from './../index.data.js'
</script>
<template>
<div class="container mx-auto flex flex-wrap justify-center gap-x-4 gap-y-4">
<ArticleCard v-for="article in articles" :title="article.title" :href="article.url" :date="article.date.string" :image="article.image" :excerpt="article.excerpt"/>
</div>
</template>

Let’s not forget some minors changes:

  • Add meta data to the top of our article article-01.md. They’ll be displayed in the article card.
---
title: My new Article
date: 2023-05-25
description: Non voluptas deleniti non laudantium magnam ea culpa ipsam At reiciendis officia aut nulla placeat qui magnam natus.
---
  • Change the image name to the same name as the article: thumbnail-01.png becomes article-01.png . The image is now automatically loaded from the article filename.
  • Register our new component in index.js:
import DefaultTheme from 'vitepress/theme-without-fonts'
import './css/custom.css'
import ArticleCard from "./components/ArticleCard.vue";
import CardContainer from "./components/CardContainer.vue";

export default {
extends: DefaultTheme,
enhanceApp(ctx) {
ctx.app.component('ArticleCard', ArticleCard);
ctx.app.component('CardContainer', CardContainer);
}
}

The only thing left is to include our new component in index.md :

---
# https://vitepress.dev/reference/default-theme-home-page
layout: home

hero:
name: "My Awesome Blog"
text: "Talking about the future !"
tagline: My great project tagline
---

<CardContainer/>

Check out that final result !:

Fully automated landing page for our blog

The newer articles are automatically loaded from top to bottom.

Going Further

You can find the complete example in our github repo: https://github.com/PDF-Toolkit-API/vitepress-blog-example

You can see the blog I have created with the same method for our SaaS company PDF Toolkit API: https://blog.pdftoolkitapi.com/

--

--