Showcase Your GitHub Projects on Your VitePress Blog

- Lire en français
Resources: garabit

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.

md
---
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:

vue
<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:

vue
<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.

Our new empty projects page
Our new empty projects page

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:

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:

vue
<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:

bash
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.

ts
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:

ts
import { Octokit } from 'octokit'

export default {
  async load() {
    // ...

    const octokit = new Octokit()
  }
}

Fetch the data by iterating over each project in every category:

ts
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:

ts
// ...

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.

  1. Create a .env file in the VitePress source directory, which is the Vite root directory, thus in the src directory.

Warning

Do not commit this file. Add it to your .gitignore file.

  1. 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.

  1. Load this token in the github-projects.data.ts file:
ts
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:

vue
<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:

ts
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:

ts
// ...
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:

vue
<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.

Our new projects page with GitHub projects
Our new projects page with GitHub projects

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!