Part 7 of 16
Projects with the GitHub API
Load the repositories live, cache them in the browser and handle loading and error states.
The best thing about a personal website is not having to update projects by hand: the page reads them straight from GitHub.
The endpoint
The public GitHub API lets you list a user’s repos:
https://api.github.com/users/your-user/repos?per_page=100&sort=updated
It returns JSON with all the repos and their metadata: name, description, language, stars, forks, link…
The github.ts module
A file with the logic, typed with TypeScript and with a cache so we don’t call the API more than necessary (the unauthenticated version has a requests-per-hour limit):
export async function getRepos(username: string) {
const cacheKey = `github-repos:${username}`;
const cached = readCache(cacheKey);
if (cached) return cached;
const response = await fetch(
`https://api.github.com/users/${username}/repos?per_page=100&sort=updated`
);
if (!response.ok) throw new Error(`GitHub API: ${response.status}`);
const repos = await response.json();
writeCache(cacheKey, repos);
return repos;
}
The cache stores the result in localStorage with a timestamp and invalidates it after 24 hours:
const TTL = 24 * 60 * 60 * 1000;
function readCache(key: string) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const data = JSON.parse(raw);
if (Date.now() - data.fetchedAt > TTL) {
localStorage.removeItem(key);
return null;
}
return data.repos;
}
function writeCache(key: string, repos: unknown[]) {
localStorage.setItem(key, JSON.stringify({ fetchedAt: Date.now(), repos }));
}
The projects page
The page shows an initial “Loading…” state, then the cards. If something fails, it shows a message with a retry button. All of it in a <script> on the page:
<script>
import { getRepos } from '../scripts/github';
const container = document.querySelector('#projects');
const username = container?.getAttribute('data-username') ?? '';
async function load() {
if (!username) {
setStatus('The GitHub user is not configured.');
return;
}
try {
const repos = await getRepos(username);
renderCards(repos);
} catch {
setStatus('Couldn\'t load the projects. Try again later.');
}
}
void load();
</script>
The username is passed without hardcoding it in the JS, through a data-username attribute that the server fills from the environment variable.
Security detail: when painting data that comes from outside (the API) with
innerHTML, I always sanitize the text to escape<,>,&… so a weird repo name can’t turn into malicious HTML.
In the next part, everything search engines need to find the site.