Astro component for a list of GitHub gists
Using the GitHub JS client Octokit to fetch all gists for a user and render them using an Astro component.
TLDR
Created an Astro component to list all the gists created by a specific GitHub user.
How it looks
You can see it on the about me page of this site, but here’s a screenshot: One day I’ll set up MDX on this site and just put the actual component there…
The code
<section>
<h3>My GitHub Gists</h3>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3" id="gists"></div>
</section>
<script>
// https://github.com/octokit/octokit.js
import { Octokit } from "octokit";
// To generate token: https://docs.github.com/en/enterprise-server@3.9/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
const octokit = new Octokit({
auth: import.meta.env.PUBLIC_GITHUB_TOKEN,
});
// Fetch our gists with Octokit + handle error if fetch fails
async function getGists() {
// Modify username here
return await octokit
.request("GET /users/ChrisLaRocque/gists", {
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
})
.catch(() => {
// Show error in UI if fetch fails
const oops = document.createElement("p");
oops.innerText = "Oops, couldn't fetch gists from GitHub";
gistsDiv?.appendChild(oops);
});
}
// Get the parent element we'll add our gists to
const gistsDiv = document.getElementById("gists");
// Run fetch above
const { data } = await getGists();
// Loop through each returned gist and create a card for it
for (let i = 0; i < data.length; i++) {
const { id, description, updated_at, html_url, files } = data[i];
// The 'files' array contains objects where the key is the file name, so we get each key to show the names of the file
const fileNames = Object.keys(files);
// Create card as an anchor tag
const gistCard = document.createElement("a");
gistCard.setAttribute("id", id);
gistCard.setAttribute("href", html_url);
gistCard.setAttribute(
"class",
"p-4 border-2 border-white rounded flex flex-col justify-between"
);
// Create + append file name heading
const heading = document.createElement("h4");
heading.setAttribute(
"class",
"text-md font-bold tracking-tight text-slate-400"
);
heading.innerText = fileNames[0];
gistCard.appendChild(heading);
// Create + append description text
const text = document.createElement("p");
text.setAttribute("class", "text-sm");
text.innerText = description;
gistCard.appendChild(text);
// Create + append update at text
const updatedAt = document.createElement("small");
updatedAt.setAttribute("class", "text-slate-400");
updatedAt.innerText = new Date(updated_at).toLocaleDateString(undefined, {
day: "numeric",
month: "short",
year: "numeric",
});
gistCard.appendChild(updatedAt);
// Add card div to parent
gistsDiv?.appendChild(gistCard);
}
</script>
Fetching list of Gists with Oktokit
You’ll need to install octokit if you haven’t already:
npm install octokit
Generating a token
To generate a token:
- Make sure you already have a
.env
file already created to paste your token in - Go to https://github.com/settings/tokens
- Click ‘Generate new token’ > ‘Generate new token (classic)’
- Select
read:user
from the available permissions. You can set whatever you like for expiry and note. - Copy + paste the token directly to your
.env
file.
In my example I named the token PUBLIC_GITHUB_TOKEN
. Note that in Astro specifically if we want to use an environment variable on the client we need to prefix it with PUBLIC_
, if using another framework you may need to follow a similar convention.
The getGists
function
import { Octokit } from "octokit";
const octokit = new Octokit({
auth: import.meta.env.PUBLIC_GITHUB_TOKEN,
});
// Fetch our gists with Octokit + handle error if fetch fails
async function getGists() {
// Modify username here
return await octokit
.request("GET /users/ChrisLaRocque/gists", {
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
})
.catch(() => {
// Show error in UI if fetch fails
const oops = document.createElement("p");
oops.innerText = "Oops, couldn't fetch gists from GitHub";
gistsDiv?.appendChild(oops);
});
}
First we import octokit and initialize the client with our token from the step before.
Next with a bit of light Googling we can find the GitHub API endpoint for listing all the gists from a given user. The X-GitHub-Api-Version
header was just part of the oktokit boilerplate, assumably to version lock the API to prevent future breaking changes.
Finally we add a .catch()
to update the UI if there’s some issue with our fetch, so the user isn’t left with a blank component.
Adding gist cards to the gists
div
Here’s the full block of code responsible for taking our data and making it HTML. The high-level flow is:
- Get our array of gists (
data
fetched in the last step) - Loop through each the array to create a card for each gist
- Add each card to the parent
gistsDiv
element
// Drill down to response from `getGists` above
const { data } = await getGists();
// Loop through each returned gist and create a card for it
for (let i = 0; i < data.length; i++) {
const { id, description, updated_at, html_url, files } = data[i];
// The 'files' array contains objects where the key is the file name, so we get each key to show the names of the file
const fileNames = Object.keys(files);
// Create card as an anchor tag
const gistCard = document.createElement("a");
gistCard.setAttribute("id", id);
gistCard.setAttribute("href", html_url);
gistCard.setAttribute(
"class",
"p-4 border-2 border-white rounded flex flex-col justify-between"
);
// Create + append file name heading
const heading = document.createElement("h4");
heading.setAttribute(
"class",
"text-md font-bold tracking-tight text-slate-400"
);
heading.innerText = fileNames[0];
gistCard.appendChild(heading);
// Create + append description text
const text = document.createElement("p");
text.setAttribute("class", "text-sm");
text.innerText = description;
gistCard.appendChild(text);
// Create + append update at text
const updatedAt = document.createElement("small");
updatedAt.setAttribute("class", "text-slate-400");
updatedAt.innerText = new Date(updated_at).toLocaleDateString(undefined, {
day: "numeric",
month: "short",
year: "numeric",
});
gistCard.appendChild(updatedAt);
// Add card div to parent
gistsDiv?.appendChild(gistCard);
}
Destructure each gist item
As we loop through the array of gists in our data
response, use destructuring to access the object values of each gist. These values will populate our elements in the next steps.
const { id, description, updated_at, html_url, files } = data[i];
Get names of files
GitHub’s UI shows the first file in each gist’s filename as the ‘title’ for their version of these cards, but that singular ‘title’ isn’t available in the API.
The API does however return the array of files in the gist, where each item in the array is an object with the key being the file name and the value being the contents of the file. To get just the filenames we use Object.keys()
to get the keys (file names) of our object.
const fileNames = Object.keys(files);
In this component we’re just going to get the first file name for the heading, but this files array could be used to do a lot of cool things to enhance this component! You could:
- Show the languages used in the gist on the card
- Show previews of your code
Creating the cards
Creating the a tags for each card
First we create our ‘card’ element that we’ll eventually add to our gistsDiv
, but first we’ll use the data we fetched to populate the card with information about each gist.
To start we:
- Create an
a
tag - Set the
id
to theid
provided by the GitHub API response. Not necessary for this example but doesn’t hurt us to have DOM elements be uniquely identifiable. - Set the
href
to thehtml_url
value from our GitHub data. Thehtml_url
is the URL you hit as a user to view a gist. - Set
class
with some Tailwind classes.
// Create card as an anchor tag
const gistCard = document.createElement("a");
gistCard.setAttribute("id", id);
gistCard.setAttribute("href", html_url);
gistCard.setAttribute(
"class",
"p-4 border-2 border-white rounded flex flex-col justify-between"
);
Adding the file name heading
Next we:
- Create an
h4
element - Set the class to some Tailwind classes
- Set the text for the element to the first file in the gist (as shown in “Get names of files” step above)
- Add the element to our parent card
// Create + append file name heading
const heading = document.createElement("h4");
heading.setAttribute(
"class",
"text-md font-bold tracking-tight text-slate-400"
);
heading.innerText = fileNames[0];
gistCard.appendChild(heading);
Adding the description text
Adding the description is similar to adding the heading:
- Create a
p
element - Set the class to some Tailwind classes
- Set the text for the element to the description
- Add the element to our parent card
// Create + append description text
const text = document.createElement("p");
text.setAttribute("class", "text-sm");
text.innerText = description;
gistCard.appendChild(text);
Adding updatedAt
text
Finally (and similarly to our last 2 steps), we add the updatedAt
date to our card:
- Create a
small
element - Set the class to some Tailwind classes
- Set the text for the element to a JS
Date
object with ourupdatedAt
string. We then parse thatDate
object as a locale string with some formatting options. - Add the element to our parent card
// Create + append update at text
const updatedAt = document.createElement("small");
updatedAt.setAttribute("class", "text-slate-400");
updatedAt.innerText = new Date(updated_at).toLocaleDateString(undefined, {
day: "numeric",
month: "short",
year: "numeric",
});
gistCard.appendChild(updatedAt);
Adding the card to the parent gists
div
Finally with all our child elements added, we add the card to the parent gistsDiv
gistsDiv?.appendChild(gistCard);
A note on CLS
If you’re unaware what CLS is: https://web.dev/articles/cls
This component pretty recklessly starts with an empty div and pops elements in as our API response comes back. As such, this component may create a negative impact on your CLS.
To counteract the pop-in of content you could create the grid of gist cards as placeholder ‘dummy’ content, and then update our script to simply change the text content of the ‘dummy’ cards.
A note on hover state
This component doesn’t have a hover state besides changing to cursor: pointer
. Feel free to make it snazzier!