Showcase Your GitHub Projects on Your VitePress Blog
In today's tutorial, we will extend our exploration of VitePress data loaders, initiated in the episode on constructing a blog index page. This time, our focus will be on highlighting your GitHub projects by utilizing the GitHub API. Exciting, isn't it?
Crafting a New Page
As ever, a new page begins with a fresh Markdown file. Create a file entitled projects.md
in the src
directory. This file, akin to the blog index page, will only contain frontmatter since the content will be dynamically loaded.
---
layout: projects
title: Projects
description: Explore my projects.
---
Next, we need to create the Vue component that will format our page. Insert a new file named ProjectsIndex.vue
in the .vitepress/theme/pages/Projects
directory. This component will resemble the BlogIndex.vue
component:
<template>
projects
</template>
Finally, link the newly created page to the layout projects
specified in the projects.md
file. To accomplish this, open the Layout.vue
file and incorporate this page:
<template>
<div class="min-h-screen bg-[#FC88FF] py-16">
<main class="mx-auto max-w-screen-md">
<BlogIndex v-if="frontmatter.layout === 'blog'" />
<BlogShow v-else-if="frontmatter.layout === 'blog-show'" />
<ProjectsIndex v-else-if="frontmatter.layout === 'projects'" />
</main>
</div>
</template>
Navigate to http://localhost:3000/projects to view the new page, which appears rather empty at the moment.
Retrieving GitHub Projects
Now, we need to retrieve data from GitHub to showcase on this page. We will use both the GitHub API for accessing up-to-date information and the VitePress data loaders to fetch data only during build time. Real-time data is unnecessary for this purpose.
First, establish a loader in .vitepress/data/github-projects.data.ts
:
export default {
async load() {
return 'hello projects'
}
}
Remember that the .data.ts
extension is essential to notify VitePress that this file functions as a data loader. 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.
Then, load this file in the ProjectsIndex.vue
component:
<script lang="ts" setup>
import { data } from '../../../data/github-projects.data'
</script>
<template>
{{ data }}
</template>
Our page now displays the text hello projects
. It's a promising beginning!
With this setup, we can now retrieve the GitHub projects data. Return to our github-projects.data.ts
file and fetch data from the GitHub API. To facilitate this, we will install the octokit
package:
pnpm add -D octokit
Then, edit the github-projects.data.ts
file. To ease future modifications, let's create an array organizing all our projects into categories.
Note
This is an example, but remember that you can structure the data as you prefer.
export default {
async load() {
const projects = [
{
title: 'Website',
projects: ['barbapapazes/orion'],
},
{
title: 'Nuxt Modules',
projects: [
'barbapapazes/nuxt-authorization',
'barbapapazes/nuxt-payload-analyzer',
],
},
{
title: 'Nuxt Templates',
projects: [
'barbapapazes/gavarnie',
'barbapapazes/slantire',
'barbapapazes/the-green-chronicle',
],
},
{
title: 'Tools',
projects: ['barbapapazes/utils-ai'],
},
]
}
}
Next, create an Octokit instance for fetching data:
import { Octokit } from 'octokit'
export default {
async load() {
// ...
const octokit = new Octokit()
}
}
Fetch the data by iterating over each project in every category:
export default {
async load() {
// ...
const loadedProjects = await Promise.all(
projects.map(async (project) => {
const projects = await Promise.all(
project.projects.map(async (project) => {
const [owner, repo] = project.split('/')
const { data } = await octokit.rest.repos.get({
owner,
repo,
})
return {
name: project,
description: data.description,
url: data.html_url,
}
}),
)
return { ...project, projects }
}),
)
return loadedProjects
}
}
To provide autocompletion in the Vue component loading this file, we can export the data type:
// ...
declare const data: {
title: string
projects: {
name: string
description: string
url: string
}[]
}[]
export { data }
// ...
Exceed the Rate Limit
Without authentication, the GitHub API has a rate limit of 60 requests per hour. Even though data is fetched only at server startup, not on every page load, it's easy to hit this limit during development. To prevent this and increase the limit to 5,000 requests per hour, we can assign a personal access token to the Octokit instance.
- Create a
.env
file in the VitePress source directory, which is the Vite root directory, thus in thesrc
directory.
Warning
Do not commit this file. Add it to your .gitignore
file.
- Insert your personal access token into this file:
VITE_GITHUB_TOKEN=your-personal-access-token
Create a personal access token by following the GitHub documentation or using the GitHub CLI gh auth token
.
- Load this token in the
github-projects.data.ts
file:
import { join } from 'node:path'
import { Octokit } from 'octokit'
import { loadEnv } from 'vitepress'
export default {
async load() {
// ...
const env = loadEnv('', join(process.cwd(), 'src'))
const octokit = new Octokit({ auth: env.VITE_GITHUB_TOKEN })
// ...
}
}
No more concerns about rate limit issues!
Displaying the Projects
Having set up the page and gathered the data, it's time to display them.
First, create a component named ProjectCard.vue
in the .vitepress/theme/components
directory. It will be similar to the BlogCard.vue
component:
<script lang="ts" setup>
import type { Project } from '../types/project'
defineProps<{
project: Project
}>()
</script>
<template>
<article
class="group relative border-4 border-black bg-white p-6 shadow-[8px_8px_0_black] transition duration-150 ease-linear space-y-4 hover:shadow-[12px_12px_0_black] hover:-translate-x-1 hover:-translate-y-1"
>
<h2 class="text-2xl text-black font-medium">
<a :href="project.url">
{{ project.name }}
<span class="absolute inset-0" />
</a>
</h2>
<p v-if="project.description" class="text-lg text-black">
{{ project.description }}
</p>
</article>
</template>
As observed, we are utilizing a Project
type, which we need to establish in the types/project.ts
file:
export interface Project {
name: string
description: string | null
url: string
}
The great aspect is that we can apply this type in the github-projects.data.ts
file to ensure the data retrieved from the GitHub API aligns with the expected format for display. This is an effective strategy to prevent errors if we modify the data structure in one location but forget to update elsewhere.
In the github-projects.data.ts
file, we can now import this type and utilize it:
// ...
import { Project } from '../theme/types/project'
declare const data: {
title: string
projects: Project[]
}[]
export { data }
export default {
async load() {
// ...
const loadedProjects = await Promise.all(
projects.map(async (project) => {
const projects = await Promise.all(
project.projects.map(async (project) => {
const [owner, repo] = project.split('/')
const { data } = await octokit.rest.repos.get({
owner,
repo,
})
return {
name: project,
description: data.description,
url: data.html_url,
} satisfies Project
}),
)
return { ...project, projects }
}),
)
return loadedProjects
}
}
Lastly, display the data in the ProjectsIndex.vue
component:
<script lang="ts" setup>
import { data } from '../../../data/github-projects.data'
const { frontmatter } = useData()
</script>
<template>
<section class="space-y-16">
<h1 class="text-6xl font-semibold">
{{ frontmatter.title }}
</h1>
<div v-for="section in data" :key="section.title" class="space-y-20">
<section class="space-y-10">
<h2 class="text-4xl font-semibold">
{{ section.title }}
</h2>
<ProjectCard
v-for="project in section.projects"
:key="project.url"
:project="project"
/>
</section>
</div>
<div class="flex justify-center">
<ButtonPrimary href="/">
Back to Home
</ButtonPrimary>
</div>
</section>
</template>
This component is straightforward. It iterates over each category of projects and displays them using the ProjectCard
component. Additionally, a button leading back to the home page is included, consistent with other pages. The imported data is straightforward to consume due to its definition in the github-projects.data.ts
file, minimizing the need for manipulation within the component. This methodology is preferable to maintain simplicity in the component and place logic at build time.
With this practical example, we now have a deeper understanding of the versatility and efficacy of VitePress data loaders. They even allow integration with a headless CMS to display your content or a database to access your data. The possibilities are truly limitless!