commit 41f1cc879676851b4b92c9e978533993e6750170
parent 1853e1f14be3a7c8a16daf6809a147c168ac249b
Author: Damian Myrda <damian@prime8.dev>
Date: Wed, 21 Jan 2026 15:48:37 -0600
refactor: consolidate git API fetching for dates and readmes
Diffstat:
13 files changed, 109 insertions(+), 124 deletions(-)
diff --git a/web/config/projects.json b/web/config/projects.json
@@ -5,12 +5,7 @@
"subtitle": "Audio classification with AST",
"tech": ["Rust", "CUDA"],
"url": "https://git.prime8.dev/moncheeta/janitor",
- "readmeSource": {
- "url": "https://git.prime8.dev/moncheeta/janitor/-/raw/master/README.md",
- "baseUrl": "https://git.prime8.dev/moncheeta/janitor/-/raw/master"
- },
- "dateCreated": "2025-09-15",
- "dateModified": "2026-01-10"
+ "apiUrl": "https://git.prime8.dev/api/v4/projects/moncheeta%2Fjanitor"
},
{
"id": "summarizer",
@@ -18,12 +13,7 @@
"subtitle": "Audio transcription microservices",
"tech": ["Python", "Docker", "Kafka"],
"url": "https://git.prime8.dev/moncheeta/summarizer",
- "readmeSource": {
- "url": "https://git.prime8.dev/moncheeta/summarizer/-/raw/master/README.md",
- "baseUrl": "https://git.prime8.dev/moncheeta/summarizer/-/raw/master"
- },
- "dateCreated": "2025-06-01",
- "dateModified": "2025-12-20"
+ "apiUrl": "https://git.prime8.dev/api/v4/projects/moncheeta%2Fsummarizer"
},
{
"id": "finger",
@@ -31,12 +21,7 @@
"subtitle": "Mobile auth app",
"tech": ["TypeScript", "React Native", "Supabase"],
"url": "https://git.prime8.dev/moncheeta/finger",
- "readmeSource": {
- "url": "https://git.prime8.dev/moncheeta/finger/-/raw/master/README.md",
- "baseUrl": "https://git.prime8.dev/moncheeta/finger/-/raw/master"
- },
- "dateCreated": "2025-03-10",
- "dateModified": "2025-08-15"
+ "apiUrl": "https://git.prime8.dev/api/v4/projects/moncheeta%2Ffinger"
},
{
"id": "computer-science-club",
@@ -44,11 +29,6 @@
"subtitle": "CS Club website",
"tech": ["Python"],
"url": "https://git.prime8.dev/moncheeta/computer-science-club",
- "readmeSource": {
- "url": "https://git.prime8.dev/moncheeta/computer-science-club/-/raw/master/README.md",
- "baseUrl": "https://git.prime8.dev/moncheeta/computer-science-club/-/raw/master"
- },
- "dateCreated": "2024-09-01",
- "dateModified": "2025-05-20"
+ "apiUrl": "https://git.prime8.dev/api/v4/projects/moncheeta%2Fcomputer-science-club"
}
]
diff --git a/web/src/components/pages/Projects.astro b/web/src/components/pages/Projects.astro
@@ -1,12 +1,22 @@
---
import rawProjects from "../../../config/projects.json";
import type Project from "../../lib/types/project";
-import { fetchAllReadmes } from "../../lib/readme/fetch";
+import { fetchGitMetadataForProject } from "../../lib/git";
+import { parseMarkdown } from "../../lib/readme";
import { getTagClass, getTechMeta } from "../../lib/utils/tags";
const projects = rawProjects as Project[];
-const readmes = await fetchAllReadmes();
+const metadata = Object.fromEntries(
+ (
+ await Promise.all(
+ projects.map(async (project) => [
+ project.id,
+ await fetchGitMetadataForProject(project),
+ ]),
+ )
+ ).filter(([, meta]) => meta !== null),
+);
---
<section id="projects" class="page">
@@ -52,8 +62,8 @@ const readmes = await fetchAllReadmes();
))}
</div>
<div class="readme-content" data-readme={prg.id}>
- {readmes?.[prg.id] ? (
- <Fragment set:html={readmes[prg.id]} />
+ {metadata?.[prg.id]?.readme ? (
+ <Fragment set:html={parseMarkdown(metadata[prg.id].readme)} />
) : (
<p class="color-dim">README not available</p>
)}
diff --git a/web/src/lib/blogs.ts b/web/src/lib/blogs.ts
@@ -1,5 +1,5 @@
import matter from "gray-matter";
-import { parseMarkdown } from "./readme/render";
+import { parseMarkdown } from "./readme";
import type BlogPost from "./types/blog";
const blogModules = import.meta.glob<string>("../../blogs/*.md", {
diff --git a/web/src/lib/git.ts b/web/src/lib/git.ts
@@ -0,0 +1,63 @@
+import type Project from "./types/project";
+import type { GitMetadata } from "./types/git";
+
+export async function fetchGitMetadataForProject(
+ project: Project,
+): Promise<GitMetadata | null> {
+ try {
+ const response = await fetch(project.apiUrl, {
+ headers: { "User-Agent": "prime8dev-portfolio" },
+ });
+
+ if (!response.ok) return null;
+
+ const data = await response.json();
+
+ const baseUrl = `${data.web_url.replace("http://", "https://")}/-/raw${data.default_branch}`;
+
+ return {
+ readme: await fetchReadme(baseUrl, toRawUrl(data.readme_url)),
+ dateCreated: data.created_at,
+ dateModified: data.updated_at,
+ };
+ } catch {
+ return null;
+ }
+}
+
+function toRawUrl(blobUrl: string): string {
+ return blobUrl.replace("http://", "https://").replace("/-/blob/", "/-/raw/");
+}
+
+async function fetchReadme(
+ baseUrl: string,
+ url: string,
+): Promise<string | null> {
+ try {
+ const response = await fetch(url, {
+ headers: { "User-Agent": "prime8dev-portfolio" },
+ });
+ if (!response.ok) return null;
+
+ const content = await response.text();
+ return fixRelativePaths(baseUrl, content);
+ } catch {
+ return null;
+ }
+}
+
+function fixRelativePaths(baseUrl: string, markdown: string): string {
+ // Fix relative image paths:  -> 
+ let fixed = markdown.replace(
+ /!\[([^\]]*)\]\(\.\/([^)]+)\)/g,
+ ``,
+ );
+
+ // Fix relative link paths (not images): [text](./path) -> [text](baseUrl/path)
+ fixed = fixed.replace(
+ /(?<!!)\[([^\]]+)\]\(\.\/([^)]+)\)/g,
+ `[$1](${baseUrl}/$2)`,
+ );
+
+ return fixed;
+}
diff --git a/web/src/lib/readme/render.ts b/web/src/lib/readme.ts
diff --git a/web/src/lib/readme/fetch.ts b/web/src/lib/readme/fetch.ts
@@ -1,69 +0,0 @@
-import { parseMarkdown, removeFirstH1 } from "./render";
-import rawProjects from "../../../config/projects.json";
-import type Project from "../types/project";
-import type { ReadmeMap, ReadmeSourceUrl } from "../types/readme";
-
-const projects = rawProjects as Project[];
-
-export async function fetchAllReadmes(): Promise<ReadmeMap> {
- const results = await Promise.all(
- projects.map(async (project) => [
- project.id,
- await fetchReadmeForProject(project),
- ]),
- );
-
- return Object.fromEntries(results.filter(([, readme]) => readme !== null));
-}
-
-export async function fetchReadmeForProject(
- project: Project,
-): Promise<string | null> {
- const content = await fetchReadmeContent(project.readmeSource);
- if (!content) return null;
-
- return parseMarkdown(removeFirstH1(content));
-}
-
-async function fetchReadmeContent(
- source: Project["readmeSource"],
-): Promise<string | null> {
- if (typeof source === "string") {
- return source;
- }
-
- const content = await fetchUrl(source.url);
- if (!content) return null;
-
- return fixRelativePaths(source.baseUrl, content);
-}
-
-async function fetchUrl(url: string): Promise<string | null> {
- try {
- const response = await fetch(url, {
- headers: { "User-Agent": "prime8dev-portfolio" },
- });
- if (response.ok) {
- return await response.text();
- }
- } catch {
- // Ignore fetch errors
- }
- return null;
-}
-
-function fixRelativePaths(baseUrl: string, markdown: string): string {
- // Fix relative image paths:  -> 
- let fixed = markdown.replace(
- /!\[([^\]]*)\]\(\.\/([^)]+)\)/g,
- ``,
- );
-
- // Fix relative link paths (not images): [text](./path) -> [text](baseUrl/path)
- fixed = fixed.replace(
- /(?<!!)\[([^\]]+)\]\(\.\/([^)]+)\)/g,
- `[$1](${baseUrl}/$2)`,
- );
-
- return fixed;
-}
diff --git a/web/src/lib/schemas/project.ts b/web/src/lib/schemas/project.ts
@@ -1,8 +1,9 @@
import type Project from "../types/project";
+import type { GitMetadata } from "../types/git";
import personSchema from "./person";
import { URL } from "../../constants/website";
-export function getSchemaForProject(project: Project) {
+export function getSchemaForProject(project: Project, metadata?: GitMetadata) {
return {
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
@@ -12,8 +13,8 @@ export function getSchemaForProject(project: Project) {
codeRepository: project.url,
programmingLanguage: project.tech,
author: personSchema,
- ...(project.dateCreated && { dateCreated: project.dateCreated }),
- ...(project.dateModified && { dateModified: project.dateModified }),
+ ...(metadata?.dateCreated && { dateCreated: metadata.dateCreated }),
+ ...(metadata?.dateModified && { dateModified: metadata.dateModified }),
};
}
diff --git a/web/src/lib/types/git.ts b/web/src/lib/types/git.ts
@@ -0,0 +1,9 @@
+import type Project from "./project";
+
+export interface GitMetadata {
+ readme: string | null;
+ dateCreated: Date;
+ dateModified: Date;
+}
+
+export type GitMetadataMap = Partial<Record<Project["id"], GitMetadata>>;
diff --git a/web/src/lib/types/project.ts b/web/src/lib/types/project.ts
@@ -1,12 +1,8 @@
-import type { ReadmeSource } from "./readme";
-
export default interface Project {
id: string;
name: string;
subtitle: string;
tech: string[];
url: string;
- readmeSource: ReadmeSource;
- dateCreated?: string;
- dateModified?: string;
+ apiUrl: string;
}
diff --git a/web/src/lib/types/readme.ts b/web/src/lib/types/readme.ts
@@ -1,10 +0,0 @@
-import type Project from "./project";
-
-export type ReadmeSource = string | ReadmeSourceUrl;
-
-export interface ReadmeSourceUrl {
- url: string;
- baseUrl: string;
-}
-
-export type ReadmeMap = Partial<Record<Project["id"], string>>;
diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro
@@ -15,7 +15,7 @@ import ProjectsPage from "../components/pages/Projects.astro";
import BlogPage from "../components/pages/Blog.astro";
import Footer from "../components/Footer.astro";
-const [wakaTimeData] = await Promise.all([fetchWakaTimeData()]);
+const wakaTimeData = await fetchWakaTimeData();
---
<Layout
diff --git a/web/src/pages/projects/[id].astro b/web/src/pages/projects/[id].astro
@@ -4,7 +4,8 @@ import rawProjects from "../../../config/projects.json";
import type Project from "../../lib/types/project";
import { URL } from "../../constants/website";
import { getTagClass } from "../../lib/utils/tags";
-import { fetchReadmeForProject } from "../../lib/readme/fetch";
+import { fetchGitMetadataForProject } from "../../lib/git";
+import { parseMarkdown } from "../../lib/readme";
import {
getSchemaForProject,
getBreadcrumbSchema,
@@ -22,7 +23,7 @@ if (!project) {
const keywords = project.tech.join(", ");
-const readme = await fetchReadmeForProject(project);
+const metadata = await fetchGitMetadataForProject(project);
---
<Layout
@@ -60,8 +61,8 @@ const readme = await fetchReadmeForProject(project);
<div class="readme-content">
{
- readme ? (
- <Fragment set:html={readme} />
+ metadata?.readme ? (
+ <Fragment set:html={parseMarkdown(metadata.readme)} />
) : (
<p class="color-dim">README not available</p>
)
@@ -70,7 +71,9 @@ const readme = await fetchReadmeForProject(project);
</div>
<script
type="application/ld+json"
- set:html={JSON.stringify(getSchemaForProject(project))}
+ set:html={JSON.stringify(
+ getSchemaForProject(project, metadata ?? undefined),
+ )}
/>
<script
type="application/ld+json"
diff --git a/web/src/pages/sitemap.xml.ts b/web/src/pages/sitemap.xml.ts
@@ -4,13 +4,15 @@ import rawProjects from "../../config/projects.json";
import rawExperience from "../../config/experience.json";
import type Project from "../lib/types/project";
import type Experience from "../lib/types/experience";
+import { fetchAllGitMetadata } from "../lib/git";
const projects = rawProjects as Project[];
const experiences = rawExperience as Experience[];
const siteUrl = "https://prime8.dev";
-export const GET: APIRoute = () => {
+export const GET: APIRoute = async () => {
const now = new Date().toISOString().split("T")[0];
+ const metadata = await fetchAllGitMetadata();
// Static pages
const staticPages = [
@@ -22,7 +24,7 @@ export const GET: APIRoute = () => {
url: `/projects/${project.id}`,
changefreq: "monthly",
priority: "0.7",
- lastmod: project.dateModified || project.dateCreated || now,
+ lastmod: metadata[project.id]?.dateModified ?? now,
}));
// Experience pages