prime8dev

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

commit 1853e1f14be3a7c8a16daf6809a147c168ac249b
parent 338ffcafc6624c96040006f47c3aab5590aa5b5f
Author: Damian Myrda <damian@prime8.dev>
Date:   Wed, 21 Jan 2026 15:26:38 -0600

refactor: move readme config to projects.json and improve fetch logic

Diffstat:
Mweb/config/projects.json | 37++++++++++++++++++++-----------------
Mweb/src/components/pages/Projects.astro | 7++-----
Mweb/src/lib/readme/fetch.ts | 137++++++++++++++++++++++---------------------------------------------------------
Mweb/src/lib/types/project.ts | 3+++
Aweb/src/lib/types/readme.ts | 10++++++++++
Mweb/src/pages/index.astro | 8++------
Mweb/src/pages/projects/[id].astro | 5+++--
7 files changed, 77 insertions(+), 130 deletions(-)

diff --git a/web/config/projects.json b/web/config/projects.json @@ -3,11 +3,12 @@ "id": "janitor", "name": "janitor", "subtitle": "Audio classification with AST", - "tech": [ - "Rust", - "CUDA" - ], + "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" }, @@ -15,12 +16,12 @@ "id": "summarizer", "name": "summarizer", "subtitle": "Audio transcription microservices", - "tech": [ - "Python", - "Docker", - "Kafka" - ], + "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" }, @@ -28,12 +29,12 @@ "id": "finger", "name": "finger", "subtitle": "Mobile auth app", - "tech": [ - "TypeScript", - "React Native", - "Supabase" - ], + "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" }, @@ -41,10 +42,12 @@ "id": "computer-science-club", "name": "computer-science-club", "subtitle": "CS Club website", - "tech": [ - "Python" - ], + "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" } diff --git a/web/src/components/pages/Projects.astro b/web/src/components/pages/Projects.astro @@ -1,15 +1,12 @@ --- import rawProjects from "../../../config/projects.json"; import type Project from "../../lib/types/project"; +import { fetchAllReadmes } from "../../lib/readme/fetch"; import { getTagClass, getTechMeta } from "../../lib/utils/tags"; const projects = rawProjects as Project[]; -interface Props { - readmes?: Record<string, string>; -} - -const { readmes } = Astro.props; +const readmes = await fetchAllReadmes(); --- <section id="projects" class="page"> diff --git a/web/src/lib/readme/fetch.ts b/web/src/lib/readme/fetch.ts @@ -1,66 +1,58 @@ import { parseMarkdown, removeFirstH1 } from "./render"; +import rawProjects from "../../../config/projects.json"; +import type Project from "../types/project"; +import type { ReadmeMap, ReadmeSourceUrl } from "../types/readme"; -export async function fetchAllReadmes(): Promise<Record<string, string>> { - const readmes: Record<string, string> = {}; +const projects = rawProjects as Project[]; - const results = await Promise.allSettled( - README_CONFIGS.map(async (config) => { - const result = await fetchWithFallback(config.sources); - if (result) { - const fixed = fixRelativePaths(result.content, result.baseUrl); - const html = parseMarkdown(removeFirstH1(fixed)); - return { name: config.name, html }; - } - return { name: config.name, html: "" }; - }), +export async function fetchAllReadmes(): Promise<ReadmeMap> { + const results = await Promise.all( + projects.map(async (project) => [ + project.id, + await fetchReadmeForProject(project), + ]), ); - for (const result of results) { - if (result.status === "fulfilled" && result.value.html) { - readmes[result.value.name] = result.value.html; - } - } - - return readmes; + return Object.fromEntries(results.filter(([, readme]) => readme !== null)); } export async function fetchReadmeForProject( - projectId: string, + project: Project, ): Promise<string | null> { - const config = README_CONFIGS.find((c) => c.name === projectId); - if (!config) return null; + const content = await fetchReadmeContent(project.readmeSource); + if (!content) return null; - const result = await fetchWithFallback(config.sources); - if (result) { - const fixed = fixRelativePaths(result.content, result.baseUrl); - return parseMarkdown(removeFirstH1(fixed)); + return parseMarkdown(removeFirstH1(content)); +} + +async function fetchReadmeContent( + source: Project["readmeSource"], +): Promise<string | null> { + if (typeof source === "string") { + return source; } - return null; + + const content = await fetchUrl(source.url); + if (!content) return null; + + return fixRelativePaths(source.baseUrl, content); } -async function fetchWithFallback(sources: ReadmeSource[]): Promise<{ - content: string; - baseUrl: string; -} | null> { - for (const source of sources) { - try { - const response = await fetch(source.url, { - headers: { "User-Agent": "prime8dev-portfolio" }, - }); - if (response.ok) { - return { - content: await response.text(), - baseUrl: source.baseUrl, - }; - } - } catch { - // Try next source +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(markdown: string, baseUrl: string): string { +function fixRelativePaths(baseUrl: string, markdown: string): string { // Fix relative image paths: ![alt](./path) -> ![alt](baseUrl/path) let fixed = markdown.replace( /!\[([^\]]*)\]\(\.\/([^)]+)\)/g, @@ -75,58 +67,3 @@ function fixRelativePaths(markdown: string, baseUrl: string): string { return fixed; } - -interface ReadmeConfig { - name: string; - sources: ReadmeSource[]; -} - -interface ReadmeSource { - url: string; - baseUrl: string; -} - -const README_CONFIGS: ReadmeConfig[] = [ - { - name: "janitor", - sources: [ - { - url: "https://git.prime8.dev/moncheeta/janitor/-/raw/master/README.md", - baseUrl: "https://git.prime8.dev/moncheeta/janitor/-/raw/master", - }, - ], - }, - { - name: "summarizer", - sources: [ - { - url: "https://git.prime8.dev/moncheeta/summarizer/-/raw/master/README.md", - baseUrl: "https://git.prime8.dev/moncheeta/summarizer/-/raw/master", - }, - ], - }, - { - name: "finger", - sources: [ - { - url: "https://git.prime8.dev/moncheeta/finger/-/raw/master/README.md", - baseUrl: "https://git.prime8.dev/moncheeta/finger/-/raw/master", - }, - ], - }, - { - name: "computer-science-club", - sources: [ - { - url: "https://raw.githubusercontent.com/moncheeta/computer-science-club/main/README.md", - baseUrl: - "https://cdn.jsdelivr.net/gh/moncheeta/computer-science-club@main", - }, - { - url: "https://raw.githubusercontent.com/moncheeta/computer-science-club/master/README.md", - baseUrl: - "https://cdn.jsdelivr.net/gh/moncheeta/computer-science-club@master", - }, - ], - }, -]; diff --git a/web/src/lib/types/project.ts b/web/src/lib/types/project.ts @@ -1,9 +1,12 @@ +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; } diff --git a/web/src/lib/types/readme.ts b/web/src/lib/types/readme.ts @@ -0,0 +1,10 @@ +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 @@ -1,7 +1,6 @@ --- import Layout from "../layouts/Layout.astro"; import { fetchWakaTimeData } from "../lib/wakatime"; -import { fetchAllReadmes } from "../lib/readme/fetch"; import about from "../../config/about.json"; import currentJob from "../lib/utils/job"; @@ -16,10 +15,7 @@ import ProjectsPage from "../components/pages/Projects.astro"; import BlogPage from "../components/pages/Blog.astro"; import Footer from "../components/Footer.astro"; -const [wakaTimeData, readmes] = await Promise.all([ - fetchWakaTimeData(), - fetchAllReadmes(), -]); +const [wakaTimeData] = await Promise.all([fetchWakaTimeData()]); --- <Layout @@ -44,7 +40,7 @@ const [wakaTimeData, readmes] = await Promise.all([ <main id="content"> <AboutPage wakaTimeData={wakaTimeData} /> <ExperiencePage /> - <ProjectsPage readmes={readmes} /> + <ProjectsPage /> <BlogPage isStandalone={false} /> </main> diff --git a/web/src/pages/projects/[id].astro b/web/src/pages/projects/[id].astro @@ -11,8 +11,8 @@ import { } from "../../lib/schemas/project"; const projects = rawProjects as Project[]; -const project = projects.find((p) => p.id === Astro.params.id); +const project = projects.find((p) => p.id === Astro.params.id); if (!project) { return new Response(null, { status: 404, @@ -20,8 +20,9 @@ if (!project) { }); } -const readme = await fetchReadmeForProject(project.id); const keywords = project.tech.join(", "); + +const readme = await fetchReadmeForProject(project); --- <Layout