Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,21 @@ non-draft projects. Local Markdown/MDX projects remain in `src/content/projects/

Set `GITHUB_TOKEN` in the build environment (or a local `.env` file) to use
authenticated GitHub API limits. It is only used during content loading. Missing
READMEs produce an empty body; other GitHub errors fail the build rather than
publishing incomplete content.
READMEs produce an empty body; other errors fetching the repository or README
fail the build rather than publishing incomplete content.

Project breakdowns include a metadata sidebar. Stars, forks, watchers, creation
and update dates come from the repository response; language percentages and
contributor counts use GitHub's language and paginated contributor endpoints.
Those two optional endpoints log a warning and omit their data on failure.
These values refresh at build time, with no browser API requests or exposed token.

The repository homepage supplies the live deployment link. Set `demo` to override
it and `docs` to add a documentation link in `src/data/projects.ts`. Use
`linkedPosts: ["post-id"]` to show related published posts (drafts are excluded).
Local projects support the same links and references in frontmatter; GitHub-only
statistics stay hidden unless supplied. No placeholder links or statistics are
generated.

Run the README rendering regression checks with:

Expand Down
6 changes: 5 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cloudflare from "@astrojs/cloudflare";
import mdx from "@astrojs/mdx";
import { satteri } from "@astrojs/markdown-satteri";
import { githubMarkdownPlugins } from "./src/lib/github/markdown.ts";
import { markdownShikiConfig } from "./src/lib/markdown-theme.ts";
import sitemap from "@astrojs/sitemap";

// https://astro.build/config
Expand All @@ -16,7 +17,10 @@ export default defineConfig({

session: false,

markdown: { processor: satteri(githubMarkdownPlugins) },
markdown: {
processor: satteri(githubMarkdownPlugins),
shikiConfig: markdownShikiConfig,
},

integrations: [
mdx(),
Expand Down
251 changes: 251 additions & 0 deletions src/components/ui/ProjectSidebar.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
---
import type { ProjectEntry } from "../../lib/projects/selectors";
import { getPosts } from "../../lib/posts/selectors";
import Card from "./Card.astro";

interface Props {
project: ProjectEntry;
}

const { project } = Astro.props;
const { data } = project;
const repo = "repo" in data ? data.repo : data.github;
const stats = data.repositoryStats;
const tags = [...new Set([data.category, ...data.tags, ...data.technologies])];
const linkedIds = new Set(data.linkedPosts.map((post) => post.id));
const posts = (await getPosts()).filter((post) => linkedIds.has(post.id));
const createdAt = data.createdAt ?? data.publishedAt;
const updatedAt = data.updatedAt ?? data.pushedAt;
const links = [
{ label: "github source", href: repo },
{ label: "live deployment", href: data.demo },
{ label: "docs", href: data.docs },
].filter((link) => link.href);
const formatDate = (date: Date) =>
date.toLocaleDateString("en-US", {
month: "2-digit",
day: "2-digit",
year: "numeric",
timeZone: "UTC",
});
const formatNumber = (value: number) => value.toLocaleString("en-US");
const formatPercentage = (value: number) =>
value < 0.1 ? "<0.1" : String(Number(value.toFixed(1)));
---

<aside class="project-sidebar" aria-label={`${data.title} metadata`}>
<section aria-labelledby="project-about">
<h2 id="project-about">About</h2>
<h1>{data.title}</h1>
{data.description && <p>{data.description}</p>}
<p class="project-status">
{data.category} / {data.status}
</p>
{stats && repo && (
<ul class="repo-stats" aria-label="GitHub repository statistics">
<li>
<a href={`${repo}/stargazers`}>
{formatNumber(stats.stars)} stars <span aria-hidden="true">★</span>
</a>
</li>
<li>
<a href={`${repo}/forks`}>{formatNumber(stats.forks)} forks</a>
</li>
<li>
<a href={`${repo}/watchers`}>
{formatNumber(stats.watchers)} watching
</a>
</li>
{stats.contributors !== undefined && (
<li>
<a href={`${repo}/graphs/contributors`}>
{formatNumber(stats.contributors)} contributors
</a>
</li>
)}
</ul>
)}
</section>

{tags.length > 0 && (
<section aria-labelledby="project-tags">
<h2 id="project-tags">Tags</h2>
<p>{tags.join(", ")}</p>
</section>
)}

<section aria-labelledby="project-posts">
<h2 id="project-posts">Related Posts</h2>
{posts.length > 0 ? (
<ul class="related-posts">
{posts.map((post) => (
<li>
<Card href={`/posts/${post.id}`}>
<span class="related-post">
<span class="related-post__title">- {post.data.title}</span>
<time datetime={post.data.publishedAt.toISOString()}>
{formatDate(post.data.publishedAt)}
</time>
<span aria-hidden="true">›</span>
</span>
</Card>
</li>
))}
</ul>
) : (
<p class="muted">No related posts yet.</p>
)}
</section>

{links.length > 0 && (
<section aria-labelledby="project-links">
<h2 id="project-links">Links</h2>
<ul class="project-links">
{links.map((link) => (
<li>
<a href={link.href}>{link.label}</a>
</li>
))}
</ul>
</section>
)}

{data.languages.length > 0 && (
<section aria-labelledby="project-languages">
<h2 id="project-languages">Languages</h2>
<ul class="project-languages">
{data.languages.map((language) => (
<li>
<span>{formatPercentage(language.percentage)}%</span>{" "}
{language.name}
</li>
))}
</ul>
</section>
)}

{(createdAt || updatedAt) && (
<footer class="project-dates">
{createdAt && (
<span>
created{" "}
<time datetime={createdAt.toISOString()}>
{formatDate(createdAt)}
</time>
</span>
)}
{updatedAt && (
<span>
updated{" "}
<time datetime={updatedAt.toISOString()}>
{formatDate(updatedAt)}
</time>
</span>
)}
</footer>
)}
</aside>

<style>
.project-sidebar {
min-width: 0;
border: var(--border-width) solid var(--color-border);
padding: var(--space-4);
display: grid;
gap: var(--space-8);
font-size: 0.8125rem;
line-height: 1.6;
overflow-wrap: anywhere;
}

@media (width > 56rem) {
.project-sidebar {
/* Clear the sticky topbar's 64px logo and top padding. */
--sidebar-top: calc(64px + var(--space-6) + var(--space-4));
position: sticky;
top: var(--sidebar-top);
align-self: start;
max-height: calc(100dvh - var(--sidebar-top) - var(--space-4));
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-border) var(--color-bg);
}
}

h2 {
margin: 0 0 var(--space-1);
font-size: 1.375rem;
font-weight: normal;
line-height: 1.3;
}

h1 {
margin: 0 0 var(--space-2);
font-size: 1rem;
font-weight: normal;
}

p,
ul {
margin: 0;
}
ul {
padding: 0;
list-style: none;
}
.project-status {
margin-top: var(--space-2);
}
.muted,
.project-status,
.project-dates {
color: var(--color-text-muted);
}
.repo-stats {
margin-top: var(--space-4);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: var(--space-3);
}
.repo-stats a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus-visible {
outline: 2px solid var(--color-text);
outline-offset: 3px;
}
.related-posts {
display: grid;
gap: var(--space-2);
}
.related-post {
display: flex;
align-items: center;
gap: var(--space-2);
}
.related-post__title {
flex: 1;
}
.related-post time {
font-size: 0.6875rem;
color: var(--color-text-muted);
white-space: nowrap;
}
.project-links a {
text-underline-offset: 3px;
}
.project-languages {
border-top: 1px dashed var(--color-border);
padding-top: var(--space-2);
}
.project-dates {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2);
font-size: 0.6875rem;
}
</style>
18 changes: 18 additions & 0 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const projectSchema = z.object({
description: z.string(),
category: z.enum(projectCategories).default("software"),
publishedAt: z.coerce.date().optional(),
createdAt: z.coerce.date().optional(),
updatedAt: z.coerce.date().optional(),
pushedAt: z.coerce.date().optional(),
draft: z.boolean().default(false),
Expand All @@ -40,6 +41,23 @@ const projectSchema = z.object({
linkedPosts: z.array(reference("posts")).default([]),
github: z.url().optional(),
demo: z.url().optional(),
docs: z.url().optional(),
repositoryStats: z
.object({
stars: z.number().int().nonnegative(),
forks: z.number().int().nonnegative(),
watchers: z.number().int().nonnegative(),
contributors: z.number().int().nonnegative().optional(),
})
.optional(),
languages: z
.array(
z.object({
name: z.string(),
percentage: z.number().min(0).max(100),
}),
)
.default([]),
status: z.enum(projectStatuses).default("active"),
});

Expand Down
4 changes: 3 additions & 1 deletion src/data/projects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ProjectDefinition } from "../lib/projects/types";

// GitHub supplies README content, topics, stats, languages, dates, and the
// homepage link. Add demo/docs URLs or linkedPosts IDs here when needed.
export const projects: ProjectDefinition[] = [
{
slug: "mr-helpsworth",
Expand Down Expand Up @@ -103,7 +105,7 @@ export const projects: ProjectDefinition[] = [
{
slug: "giad",
title: "GIAD",
repo: "https://github.com/Hyaxon/giad-reviwer",
repo: "https://github.com/Hyaxon/giad-reviewer",
category: "tools",
visible: true,
status: "active",
Expand Down
9 changes: 5 additions & 4 deletions src/lib/github/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,16 @@ export async function githubRequest<T>(
);
}

if (response.status === 204) return undefined as T;
return response.json() as Promise<T>;
}

export class GitHubRequestError extends Error {
constructor(
public readonly status: number,
message: string,
) {
readonly status: number;

constructor(status: number, message: string) {
super(message);
this.status = status;
this.name = "GitHubRequestError";
}
}
Loading
Loading