The website for the University of Waterloo Climbing Club (wloocc), live at https://climbing.uwaterloo.ca.
This is a full rewrite of the previous Hugo-based site, built on Next.js with a content-first architecture so non-technical maintainers (next term's exec team) can update copy, photos, merch, comp results, and announcements without touching any code.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| UI | React 19, TypeScript |
| Styling | Tailwind CSS 4 |
| Fonts | Anton · Instrument Serif · Inter · JetBrains Mono (via next/font/google) |
| Hosting | Static export, served from UW CSC servers |
The site is fully static — every route in [app/](app/) prerenders at build time. No database, no server, no auth.
app/
page.tsx — Home: hero, about preview, sponsors marquee, events
preview, merch preview, membership, contact
about/ — About page with FAQ accordion
events/ — Calendar embed + recurring events + past highlights
gallery/ — Masonry gallery with lightbox
merch/ — Per-term merch showcase (current term + archive)
competition/ — End-of-term comp results (iframe embeds)
components/ — Photo, DisplayHeading, Accordion, Lightbox, Navbar,
MobileMenu, MarqueeStrip, etc.
lib/content.ts — Type-safe loaders for everything in content/
layout.tsx — Root layout: fonts, grain overlay, navbar, footer
globals.css — Design tokens (palette, type, duotone, grain)
fonts.ts — next/font configuration
opengraph-image.tsx — Dynamic OG image generation
robots.ts, sitemap.ts — SEO metadata routes
content/ — All editable site content (see below)
public/
logo/ — Logos and brand marks
media/
hero.jpg
events/ — Event/activity photos
merch/<term>/ — Per-term merch photos
sponsors/ — Sponsor logos (PNG)
grain.svg — Global noise overlay texture
Two design documents live alongside the code:
- CONTENT.md — what content the site displays, page by page, plus the schema for each editable file
- DESIGN.md — visual language: palette, typography, photo treatment, motion, section-level layout direction
All editable content lives in [content/](content/). Maintainers should never need to touch app/ or other code directories.
| File | What it controls | Edit when… |
|---|---|---|
site.json |
Club name, tagline, social URLs, membership URL, calendar ID | Membership URL changes, social handles change |
announcement.json |
Top-of-page banner (toggle + text + link) | An event signup is open / urgent message |
sponsors.json |
Logos shown in home page marquee | Adding/removing sponsors |
competitions.json |
EoT comp embed URLs, winners, sponsors | New comp results published |
merch/manifest.json |
Ordered list of term slugs (first = current) | Releasing new merch |
merch/<term>/meta.json |
Per-term merch items + image filenames | Releasing new merch |
past-events.json |
Featured past events (home + events page) | After running a notable event |
gallery.json |
Photos in the gallery page | Adding new photos |
pages/about.md |
About page prose, FAQ | Refreshing club description |
Photos go in [public/media/](public/media/) (organized by events/ and merch/<term>/); sponsor logos in [public/sponsors/](public/sponsors/). Reference them in JSON with absolute paths starting with /.
Toggling the announcement banner
- Open
content/announcement.json - Set
"enabled": trueand updatetext,link,linkLabel - Set back to
falseafter the event passes
Releasing new merch (e.g. F26)
- Drop photos into
public/media/merch/f26/named1.jpg,2.jpg,3.jpg— image 1 should be a climber action shot, 2 a duo, 3 a clear shirt-design shot (used by the asymmetric desktop grid) - Create
content/merch/f26/meta.jsonwith the term name and image paths - Prepend
"f26"tocontent/merch/manifest.jsonso it becomes the current term
Publishing new comp results
- Publish the results sheet (Google Sheets → Share → Publish to web → embed) or get the Airtable embed URL
- Prepend a new entry to
content/competitions.jsonwithterm,label,date,embedUrl,winners(by category), andsponsors - The page will render the iframe automatically. If the source isn't embeddable, use
resultsLinkinstead — a "View results" button is rendered as a fallback
Adding gallery photos
- Drop new images into
public/media/events/ - Prepend entries to
content/gallery.jsonwithfile, optionalcaption, optionalterm
pnpm install
pnpm devOpen http://localhost:3000.
pnpm build # production build
pnpm start # serve the production build locally
pnpm lint # eslint
⚠️ This project uses Next.js 16, which has breaking changes from earlier versions. If you're modifying the framework integration (layouts, image, font, metadata APIs), read the relevant guide innode_modules/next/dist/docs/first.
The site is intentionally not SaaS-flavored — it's an editorial / climbing-zine aesthetic translated into a website. See DESIGN.md for the full system, but the highlights:
- Palette: ink black
#0a0a0a+ paper off-white#fafaf7+ brand yellow#eaab00(with a softer#fbe38bfor section tints) - Type pairing: heavy condensed sans (Anton) for impact + italic serif (Instrument Serif) for accents — same combo as the F24 club shirt
- Photo treatment: SVG color-matrix duotone (ink → warm cream-yellow) on event/community shots, full color on merch. Toggleable per-image via
<Photo duotone /> - Grain: SVG noise layer fixed across the whole viewport
- Motion: restrained — sponsor marquee, reduced-motion-aware
The site builds to fully static HTML/CSS/JS and is deployed to UW CSC infrastructure. The previous Hugo version lives at the same URL (https://climbing.uwaterloo.ca); replacing it requires a CSC sysadmin update.
External dependencies the live site relies on:
- Google Calendar (
content/site.json→calendar.id) — embedded on/events - Google Sheets / Airtable (per-term
embedUrlincompetitions.json) — embedded on/competition - UW Warrior portal (
site.json→membership.url) — external link from "Join" CTAs
If any of those external sources change URL, update the JSON config — no code change needed.
To deploy, ensure you have access to our club group onthe CSC servers. If not see here
> ssh <watiam>@csclub.uwaterloo.ca
> become_club climbingclub
> bash update.sh