Skip to content

Commit 25580f1

Browse files
Strawbangclaude
andcommitted
feat: render README from GitHub on project pages
Fetch README.md at build time from each repo's main branch and render it as markdown. Falls back to longDescription if unavailable. Uses marked for HTML rendering with custom styles matching the site theme. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5ef57be commit 25580f1

3 files changed

Lines changed: 173 additions & 31 deletions

File tree

package-lock.json

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"dependencies": {
1515
"@astrojs/sitemap": "^3.7.1",
16-
"astro": "^6.0.5"
16+
"astro": "^6.0.5",
17+
"marked": "^17.0.4"
1718
}
1819
}

src/pages/projects/[slug].astro

Lines changed: 157 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Layout from '../../layouts/Layout.astro';
33
import Nav from '../../components/Nav.astro';
44
import Footer from '../../components/Footer.astro';
55
import { projects } from '../../data/projects';
6+
import { marked } from 'marked';
67
78
type Release = {
89
tag_name: string;
@@ -12,37 +13,45 @@ type Release = {
1213
};
1314
1415
export async function getStaticPaths() {
16+
async function fetchReleases(slug: string): Promise<Release[]> {
17+
try {
18+
const res = await fetch(`https://api.github.com/repos/rustkit-ai/${slug}/releases`);
19+
const data: Release[] = await res.json();
20+
return data
21+
.filter((r) => r.published_at)
22+
.sort((a, b) => new Date(b.published_at!).getTime() - new Date(a.published_at!).getTime());
23+
} catch {
24+
return [];
25+
}
26+
}
27+
28+
async function fetchReadme(slug: string): Promise<string> {
29+
try {
30+
const res = await fetch(`https://raw.githubusercontent.com/rustkit-ai/${slug}/main/README.md`);
31+
if (!res.ok) return '';
32+
const md = await res.text();
33+
return marked(md) as string;
34+
} catch {
35+
return '';
36+
}
37+
}
38+
1539
const results = await Promise.all(
1640
projects.map(async (p) => {
17-
let releases: Release[] = [];
18-
if (p.hasReleases) {
19-
try {
20-
const res = await fetch(`https://api.github.com/repos/rustkit-ai/${p.slug}/releases`);
21-
const data: Release[] = await res.json();
22-
releases = data
23-
.filter((r) => r.published_at)
24-
.sort((a, b) => new Date(b.published_at!).getTime() - new Date(a.published_at!).getTime());
25-
} catch {}
26-
}
27-
return { params: { slug: p.slug }, props: { project: p, releases } };
41+
const [releases, readme] = await Promise.all([
42+
p.hasReleases ? fetchReleases(p.slug) : Promise.resolve([]),
43+
fetchReadme(p.slug),
44+
]);
45+
return { params: { slug: p.slug }, props: { project: p, releases, readme } };
2846
})
2947
);
3048
return results;
3149
}
3250
3351
const { slug } = Astro.params;
3452
const project = Astro.props.project ?? projects.find((p) => p.slug === slug)!;
35-
36-
let releases: Release[] = Astro.props.releases ?? [];
37-
if (!Astro.props.releases && project.hasReleases) {
38-
try {
39-
const res = await fetch(`https://api.github.com/repos/rustkit-ai/${project.slug}/releases`);
40-
const data: Release[] = await res.json();
41-
releases = data
42-
.filter((r) => r.published_at)
43-
.sort((a, b) => new Date(b.published_at!).getTime() - new Date(a.published_at!).getTime());
44-
} catch {}
45-
}
53+
const releases: Release[] = Astro.props.releases ?? [];
54+
const readme: string = Astro.props.readme ?? '';
4655
4756
function formatDate(iso: string) {
4857
return new Date(iso).toLocaleDateString('en-US', {
@@ -107,14 +116,17 @@ function formatDate(iso: string) {
107116
</div>
108117
</div>
109118

110-
<!-- Description -->
119+
<!-- README / Description -->
111120
<div class="section">
112-
<div class="section-label">About</div>
113-
<div class="long-desc">
114-
{project.longDescription.split('\n\n').map((para) => (
115-
<p>{para}</p>
116-
))}
117-
</div>
121+
{readme ? (
122+
<div class="readme-content" set:html={readme} />
123+
) : (
124+
<div class="long-desc">
125+
{project.longDescription.split('\n\n').map((para) => (
126+
<p>{para}</p>
127+
))}
128+
</div>
129+
)}
118130
</div>
119131

120132
<!-- Releases -->
@@ -357,6 +369,122 @@ function formatDate(iso: string) {
357369

358370
.release-item:hover .release-arrow { transform: translateX(3px); color: var(--rust); }
359371

372+
/* README rendered markdown */
373+
.readme-content :global(h1) { display: none; }
374+
375+
.readme-content :global(h2) {
376+
font-family: 'Space Mono', monospace;
377+
font-size: 1.1rem;
378+
font-weight: 700;
379+
letter-spacing: -0.02em;
380+
color: var(--text);
381+
margin: 2.5rem 0 1rem;
382+
padding-bottom: 0.5rem;
383+
border-bottom: 1px solid var(--border);
384+
}
385+
386+
.readme-content :global(h3) {
387+
font-family: 'IBM Plex Mono', monospace;
388+
font-size: 0.9rem;
389+
font-weight: 600;
390+
color: var(--text);
391+
margin: 1.75rem 0 0.75rem;
392+
}
393+
394+
.readme-content :global(p) {
395+
font-size: 0.95rem;
396+
color: var(--text-muted);
397+
line-height: 1.8;
398+
margin-bottom: 1rem;
399+
}
400+
401+
.readme-content :global(p:has(img)) { display: none; }
402+
403+
.readme-content :global(a) {
404+
color: var(--rust);
405+
text-decoration: none;
406+
}
407+
408+
.readme-content :global(a:hover) { text-decoration: underline; }
409+
410+
.readme-content :global(code) {
411+
font-family: 'IBM Plex Mono', monospace;
412+
font-size: 0.82rem;
413+
background: var(--bg-2);
414+
border: 1px solid var(--border);
415+
border-radius: 3px;
416+
padding: 0.1em 0.4em;
417+
color: var(--text);
418+
}
419+
420+
.readme-content :global(pre) {
421+
background: var(--bg-2);
422+
border: 1px solid var(--border);
423+
border-radius: 6px;
424+
padding: 1.25rem 1.5rem;
425+
overflow-x: auto;
426+
margin: 1.25rem 0;
427+
}
428+
429+
.readme-content :global(pre code) {
430+
background: none;
431+
border: none;
432+
padding: 0;
433+
font-size: 0.82rem;
434+
color: var(--text-muted);
435+
}
436+
437+
.readme-content :global(ul),
438+
.readme-content :global(ol) {
439+
padding-left: 1.5rem;
440+
margin-bottom: 1rem;
441+
}
442+
443+
.readme-content :global(li) {
444+
font-size: 0.95rem;
445+
color: var(--text-muted);
446+
line-height: 1.8;
447+
margin-bottom: 0.25rem;
448+
}
449+
450+
.readme-content :global(table) {
451+
width: 100%;
452+
border-collapse: collapse;
453+
font-family: 'IBM Plex Mono', monospace;
454+
font-size: 0.82rem;
455+
margin: 1.25rem 0;
456+
}
457+
458+
.readme-content :global(th) {
459+
text-align: left;
460+
color: var(--text-dim);
461+
font-size: 0.72rem;
462+
text-transform: uppercase;
463+
letter-spacing: 0.08em;
464+
border-bottom: 1px solid var(--border);
465+
padding: 0.5rem 1rem;
466+
}
467+
468+
.readme-content :global(td) {
469+
color: var(--text-muted);
470+
border-bottom: 1px solid var(--border);
471+
padding: 0.6rem 1rem;
472+
}
473+
474+
.readme-content :global(blockquote) {
475+
border-left: 3px solid var(--rust-dim);
476+
padding-left: 1rem;
477+
margin: 1rem 0;
478+
color: var(--text-dim);
479+
font-size: 0.9rem;
480+
}
481+
482+
.readme-content :global(hr) {
483+
border: none;
484+
border-top: 1px solid var(--border);
485+
margin: 2rem 0;
486+
}
487+
360488
@media (max-width: 640px) {
361489
.project-page { padding: 6rem 1.25rem 3rem; }
362490
.release-item { grid-template-columns: auto 1fr auto; }

0 commit comments

Comments
 (0)