For club maintainers updating the site's content. Assumes npm run dev is running.
Edit PROJECTS in src/data/projects.ts. Each entry becomes a card on /showcase/projects/ and its own page at /showcase/projects/<slug>/.
{
slug: 'my-tool', // URL segment; must be unique
name: 'my-tool',
category: 'CLI', // matches PROJECT_FILTERS.category
status: 'Looking for Contributors', // 'Active' | 'Completed' | 'Looking for Contributors' | 'Archived'
blurb: 'One-line description.',
stack: ['Go', 'CLI'], // first item doubles as "Language" if `info` is omitted
contributors: 3,
repo: 'https://github.com/pesos/my-tool',
branch: 'main', // used to build the CONTRIBUTING.md link
// optional: featured, stats, info, goodFirstIssues, features, about, install, learningPath
}What happens when optional fields are left out:
| Field | Fallback on the detail page |
|---|---|
stats |
Stars/Forks/Open Issues shown as N/A, Contributors from contributors |
info |
Language = stack[0], Status = status |
install |
# Clone the repository + $ git clone <repo> |
about |
blurb |
features, learningPath, goodFirstIssues |
Section is hidden |
install lines are styled by prefix: # lines are shown as comments, and $ lines get a prompt.
Keep exactly one project with featured: true. projects.astro uses a non-null find(p => p.featured)! and will fail if none is featured.
If you add a new stack or category, also add it to PROJECT_FILTERS so it appears in the dropdowns.
A repo that doesn't need its own page goes in OTHER_PROJECTS instead (name, repo, blurb).
Add a markdown file to blogs/ at the repo root. Nothing else needs to change: the post gets its own page, a card on /blogs/, a search entry and (for a new tag) a filter chip.
-
Copy
blogs/_TEMPLATE.mdtoblogs/<slug>.md. The file name is the URL:blogs/my-first-pr.md→/blogs/my-first-pr/. Use lowercase and hyphens, and don't name a postresources.md(that URL is the Resources page). -
Fill in the front matter:
Field Required Notes titleyes descriptionyes Card text on /blogs/and in searchdateyes YYYY-MM-DD; the list is newest firstauthorno tagno Defaults to Community; a new value gets its own filter chipfeaturedno Listed first, with a green tag draftno trueshows the post innpm run devonly -
Write the post in markdown below the front matter. Use
##for sections; the page title comes fromtitle. Code blocks are syntax-highlighted for the language you name (e.g.```bash).
Files starting with _ are ignored. A missing or malformed required field fails the build with an error naming the file.
Posts appear on the live site when it's next built and deployed. Under npm run dev they appear as soon as the file is saved (restart the dev server once after pulling this change, since it reads the content config at startup).
Images go in blogs/images/<slug>/ and are referenced relatively, e.g. . They're converted to optimised WebP at build time.
Append to ARCHIVE_ENTRIES in src/data/archive.ts:
{
title: 'Hacktoberfest 2026',
type: 'Past Events', // 'Past Events' | 'Legacy Projects' | 'Historical Records'
category: 'Hackathon',
semester: 'Fall',
year: '2026',
date: 'Oct 2026',
blurb: '…',
href: 'https://…', // optional
}If the year is new, add it to ARCHIVE_YEARS, or the entry can't be picked from the Year dropdown. The "Total Entries" stat updates itself; "Years Spanned" and "Contributors" are hand-written strings.
| Content | File | Array | Also update |
|---|---|---|---|
| Resource link | resources.ts |
RESOURCES |
RESOURCE_FILTERS for a new category |
| "Why open source" card | guide.ts |
GUIDE_SECTIONS |
GUIDE_FILTERS for a new filter value |
| Join step | guide.ts |
JOIN_STEPS |
none |
| Activity | events.ts |
ACTIVITIES |
ACTIVITY_TYPES; keep exactly one featured: true |
| Perk | perks.ts |
PERKS |
PERK_CATEGORIES; keep exactly one featured: true |
All in src/data/site.ts:
NAV: the site map. Each section has alabel, anhref(where the nav item goes), abaseURL prefix and itschildrenpages. The nav, its dropdowns, the mobile drawer, every page'sSectionTabs, the footer's club links and the search index all read from it.FOOTER.socialLinks.LINKS: GitHub, members repo, Slack invite, Instagram, Twitter, Formspree endpoint.SITE.tagline: shown in the home hero and the footer.
The nav highlights a section on every page under its base, so Projects and Archive stays highlighted on /showcase/projects/grofer/. Dropdown and drawer links are highlighted only on an exact match.
To add a page to a section: create it under that section's folder in src/pages/, add it to the section's children in NAV, put <SectionTabs section="…" /> in its header, and add a one-line description to PAGE_DESC in src/pages/search.json.ts. Link only to pages in the same section.
No JS is needed. Use the attributes initFilters looks for:
<section data-filter-root>
<input data-filter-search />
<select data-filter-select="level">
<option value="all">Any level</option>
<option value="Beginner">Beginner</option>
</select>
<button class="chip is-active" data-filter="All" data-filter-key="cat">All</button>
<button class="chip" data-filter="Talk" data-filter-key="cat">Talk</button>
<span data-filter-count></span> shown
{items.map((it) => (
<article data-item data-cat={it.type} data-level={it.level} data-stack={it.stack.join('|')}>…</article>
))}
</section>Rules:
- The key in
data-filter-select="level"/data-filter-key="level"must match the card'sdata-level. all, an empty value, or a value starting with__turns that filter off.- Separate multiple values with
|. - Matching ignores case and one trailing
s.
/contact/ POSTs to LINKS.formspree. To send submissions somewhere else, change that URL. Any absolute http(s) action works if the endpoint accepts multipart/form-data and returns 2xx.
To add a form elsewhere, reuse this structure:
<form data-mock-form action="https://…" method="POST" novalidate>
<label class="field">
<input class="input" name="x" required />
<span class="field__error">Required.</span>
</label>
<div class="form-success">Sent.</div>
<div class="form-error" style="display:none">Failed.</div>
<button type="submit">Send</button>
</form>Without an http(s) action, the form only validates and shows .form-success. Nothing is sent.
| Want | Markup |
|---|---|
| Fade in on scroll | class="reveal", with an optional style="--d:120ms" delay |
| Count-up number | <span data-count="633+">633+</span> |
| Cursor spotlight on a card | class="card card--spotlight" |
| In-page tabs | data-tabs wrapper, data-tab="X" buttons, data-panel="X" panels |
| Accordion | .accordion__btn immediately followed by its panel element |
- Build error
Cannot read properties of undefinedon/showcase/projects/or/about/perks/: no entry hasfeatured: true. - A new card doesn't show up under a filter: its
data-<key>value doesn't match the chip or option value, or the new value is missing from the*_FILTERS/*_YEARSarray. - A new page isn't reachable: add it to its section's
childreninNAV.