prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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:
Mweb/config/projects.json | 28++++------------------------
Mweb/src/components/pages/Projects.astro | 18++++++++++++++----
Mweb/src/lib/blogs.ts | 2+-
Aweb/src/lib/git.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rweb/src/lib/readme/render.ts -> web/src/lib/readme.ts | 0
Dweb/src/lib/readme/fetch.ts | 69---------------------------------------------------------------------
Mweb/src/lib/schemas/project.ts | 7++++---
Aweb/src/lib/types/git.ts | 9+++++++++
Mweb/src/lib/types/project.ts | 6+-----
Dweb/src/lib/types/readme.ts | 10----------
Mweb/src/pages/index.astro | 2+-
Mweb/src/pages/projects/[id].astro | 13++++++++-----
Mweb/src/pages/sitemap.xml.ts | 6++++--
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: ![alt](./path) -> ![alt](baseUrl/path) + let fixed = markdown.replace( + /!\[([^\]]*)\]\(\.\/([^)]+)\)/g, + `![$1](${baseUrl}/$2)`, + ); + + // 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: ![alt](./path) -> ![alt](baseUrl/path) - let fixed = markdown.replace( - /!\[([^\]]*)\]\(\.\/([^)]+)\)/g, - `![$1](${baseUrl}/$2)`, - ); - - // 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