π§ Context
The webring directory (/webring) currently has three columns β Name / Grad / Site β and a member schema of just name, url, year, inRing. This ticket lets a member optionally list social links (GitHub, LinkedIn, Instagram, X, Email) and shows them as icons in a new Links column.
The fields are all optional, so this is backward-compatible: existing member entries without links stay valid and the build won't break. Do not make any of them required.
Schema, rendering, and icons are one ticket on purpose β splitting them leaves half-wired states.
Files you'll touch:
src/content.config.ts (the webring collection schema)
src/content/webring/example-member.json
src/pages/webring.astro (the directory table)
src/icons/*.svg (new β the brand icon SVGs; see step 4)
Don't touch: anything under src/components/ for projects, the projects pages, or the Card files β this is webring-only.
π οΈ Implementation Plan
-
Schema β in src/content.config.ts, add five optional URL fields to the webring collection:
schema: z.object({
name: z.string(),
url: z.url(),
year: z.number(), // graduation year
inRing: z.boolean(),
github: z.url().optional(),
linkedin: z.url().optional(),
email: z.email().optional(),
instagram: z.url().optional(),
x: z.url().optional(), // X (formerly Twitter)
}),
-
Example entry β add a couple of links to src/content/webring/example-member.json so the column isn't empty in dev (don't use links for a real student, example only):
{
"name": "Example Member",
"url": "https://example.com",
"year": 2025,
"inRing": true,
"github": "https://github.com/example",
"linkedin": "https://www.linkedin.com/in/example"
}
-
Directory column β in src/pages/webring.astro, add a Links <th> to the header row and a matching <td>. Define the keys once in the frontmatter and render whichever are present. Start with text links (you'll swap them for icons in step 4):
---
const socials = ['github', 'linkedin', 'email', 'instagram', 'x'] as const;
---
<th class="py-2 font-normal">Links</th>
<td class="py-3">
<span class="flex gap-2">
{
socials.map((key) =>
member.data[key] ? (
<a
href={member.data[key]} // email will likely need special handling since it needs "mailto:", not just the email value. Something like `key === 'email' ? mailto:${member.data[key]} : member.data[key]`
class="text-muted hover:text-accent"
target="_blank"
rel="noopener"
>
{key}
</a>
) : null,
)
}
</span>
</td>
email is handled differently from the URL links: its value is an address, so the link is href={mailto:${member.data.email}} (not the raw value), and its icon (see next step) is the lucide UI icon lucide:mail β "mail" is a generic glyph, not a brand, so lucide is fine here (no local SVG, unlike the brand icons). So when you build each link, special-case email for the mailto: href while the others use their value directly.
-
Icons β replace the text words with brand icons. Don't use the lucide: set for these β lucide is deprecating its brand glyphs, so they're not a safe long-term source. Instead add the brand SVGs as local icons and render them through astro-icon.
- Add the SVGs to
src/icons/ (astro-icon's local-icon folder): github.svg, linkedin.svg, instagram.svg, x.svg. Grab them from Simple Icons (CC0 brand logos). Make sure each SVG inherits color β strip any hardcoded fill so it uses currentColor β so the icons pick up text-muted / hover:text-accent from the tokens. For email, use the lucide mail icon.
- Reference them by bare filename. astro-icon renders a local icon by its name with no set prefix:
<Icon name="github" /> loads src/icons/github.svg (unlike the lucide: prefix used elsewhere in the project).
Map each social key to its icon and swap the text label for the icon. Because the link no longer has visible text, put the aria-label on the <a> (that becomes its screen-reader name) and mark the <Icon> aria-hidden (this hides the icon from screen readers only β it stays fully visible β so the link reads as just "GitHub" rather than announcing the SVG too):
---
import { Icon } from 'astro-icon/components';
const socials = [
{ key: 'github', label: 'GitHub', icon: 'github' },
{ key: 'linkedin', label: 'LinkedIn', icon: 'linkedin' },
{ key: 'email', label: 'Email', icon: 'lucide:mail' },
{ key: 'instagram', label: 'Instagram', icon: 'instagram' },
{ key: 'x', label: 'X', icon: 'x' },
] as const;
---
<a
href={member.data[key]} //special handling for email here as well, same as step 3
aria-label={label}
class="text-muted hover:text-accent"
target="_blank"
rel="noopener"
>
<Icon name={icon} class="h-4 w-4" aria-hidden="true" />
</a>
β
Acceptance Criteria
π§ Context
The webring directory (
/webring) currently has three columns β Name / Grad / Site β and a member schema of justname,url,year,inRing. This ticket lets a member optionally list social links (GitHub, LinkedIn, Instagram, X, Email) and shows them as icons in a new Links column.The fields are all optional, so this is backward-compatible: existing member entries without links stay valid and the build won't break. Do not make any of them required.
Schema, rendering, and icons are one ticket on purpose β splitting them leaves half-wired states.
Files you'll touch:
src/content.config.ts(thewebringcollection schema)src/content/webring/example-member.jsonsrc/pages/webring.astro(the directory table)src/icons/*.svg(new β the brand icon SVGs; see step 4)Don't touch: anything under
src/components/for projects, the projects pages, or theCardfiles β this is webring-only.π οΈ Implementation Plan
Schema β in
src/content.config.ts, add five optional URL fields to thewebringcollection:Example entry β add a couple of links to
src/content/webring/example-member.jsonso the column isn't empty in dev (don't use links for a real student, example only):{ "name": "Example Member", "url": "https://example.com", "year": 2025, "inRing": true, "github": "https://github.com/example", "linkedin": "https://www.linkedin.com/in/example" }Directory column β in
src/pages/webring.astro, add aLinks<th>to the header row and a matching<td>. Define the keys once in the frontmatter and render whichever are present. Start with text links (you'll swap them for icons in step 4):email is handled differently from the URL links: its value is an address, so the link is href={mailto:${member.data.email}} (not the raw value), and its icon (see next step) is the lucide UI icon lucide:mail β "mail" is a generic glyph, not a brand, so lucide is fine here (no local SVG, unlike the brand icons). So when you build each link, special-case email for the mailto: href while the others use their value directly.
Icons β replace the text words with brand icons. Don't use the
lucide:set for these β lucide is deprecating its brand glyphs, so they're not a safe long-term source. Instead add the brand SVGs as local icons and render them throughastro-icon.src/icons/(astro-icon's local-icon folder):github.svg,linkedin.svg,instagram.svg,x.svg. Grab them from Simple Icons (CC0 brand logos). Make sure each SVG inherits color β strip any hardcodedfillso it usescurrentColorβ so the icons pick uptext-muted/hover:text-accentfrom the tokens. For email, use the lucide mail icon.<Icon name="github" />loadssrc/icons/github.svg(unlike thelucide:prefix used elsewhere in the project).Map each social key to its icon and swap the text label for the icon. Because the link no longer has visible text, put the
aria-labelon the<a>(that becomes its screen-reader name) and mark the<Icon>aria-hidden(this hides the icon from screen readers only β it stays fully visible β so the link reads as just "GitHub" rather than announcing the SVG too):β Acceptance Criteria
github/linkedin/ 'email' /instagram/x.src/icons/rendered via<Icon name="..." />(astro-icon, nolucide:brand glyphs), accessible (aria-labelon the link), and inherit color viacurrentColor.<table>, not per-row grids).pnpm format:check,pnpm check, andpnpm buildall pass.