@@ -5,17 +5,36 @@ Production: https://dev-mtl.ca | Bilingual (FR/EN) conference site for /dev/mtl,
55## Commands
66
77``` bash
8- yarn dev # Vite dev server
9- yarn build # Production build (copies index.html → 404.html for GH Pages SPA routing)
10- yarn typecheck # tsc --noEmit (strict)
11- yarn lint # ESLint, zero warnings allowed (--max-warnings=0)
12- yarn format # Prettier --write
13- yarn format-check # Prettier --list-different
14- yarn test # Vitest (happy-dom environment)
8+ yarn dev # Vite dev server
9+ yarn build # Production build (copies index.html → 404.html for GH Pages SPA routing)
10+ yarn typecheck # tsc --noEmit (strict)
11+ yarn lint # ESLint, zero warnings allowed (--max-warnings=0)
12+ yarn format # Prettier --write
13+ yarn format-check # Prettier --list-different
14+ yarn test # Vitest (happy-dom environment)
1515```
1616
1717** CI runs on every push to main:** typecheck → lint → build → deploy to GitHub Pages.
18- Always run ` yarn typecheck ` and ` yarn lint ` before committing.
18+ There is no staging — pushing to ` main ` publishes to https://dev-mtl.ca . Always run ` yarn typecheck `
19+ and ` yarn lint ` before committing.
20+
21+ ` yarn format-check ` currently flags a dozen pre-existing files. Check that your own files are clean
22+ rather than expecting the whole command to pass.
23+
24+ ### QA a content change in the browser
25+
26+ Content changes (speaker, sponsor, schedule) are only really verified by looking at the page. Start the
27+ dev server through the preview tooling — never ` yarn dev ` in a shell — using the ` dev ` config already in
28+ ` .claude/launch.json ` , then open the page the change affects:
29+
30+ | Change | Page to check |
31+ | ------------------------ | ---------------------------------------- |
32+ | Speaker bio, talk, photo | ` /speaker/{slug} ` and ` /speakers/{year} ` |
33+ | Talk time or track | ` /schedule ` |
34+ | Sponsor | ` /sponsors ` and the home page |
35+
36+ Read the page text and console, screenshot the result, and stop the server when done. Verify it yourself —
37+ don't hand the URL to Nicolas and ask him to look.
1938
2039## Tech Stack
2140
@@ -54,13 +73,16 @@ public/ # Static assets (images referenced in JSON data)
5473
5574All content is JSON files in ` src/assets/ ` . Loaded via ` loadData<T>(folder) ` which uses ` import.meta.glob ` .
5675
57- To add a new year of speakers: create ` src/assets/speakers-{year}/ ` , add a glob entry in ` src/utils/loadData.ts ` , and update ` SpeakersPage.tsx ` .
76+ Speakers load through ` loadSpeakers<T>(year) ` , which globs ` speakers-* ` and filters by year prefix.
77+ ** Adding a speaker — or a whole new year — needs no code change:** drop the JSON in ` src/assets/speakers-{year}/ `
78+ and it is picked up. ` SpeakersPage ` reads the year from the route param, and ` scripts/export-schedule-to-markdown.js `
79+ does a ` readdir ` on the folder.
5880
5981### Internationalization — inline, no library
6082
6183``` tsx
6284const { t } = useTranslation ();
63- <h2 >{ t ({ fr: " Conférenciers" , en: " Speakers" })} </h2 >
85+ <h2 >{ t ({ fr: " Conférenciers" , en: " Speakers" })} </h2 >;
6486```
6587
6688All translations are inline using ` t() ` . No separate translation files. Every user-visible string must have both ` fr ` and ` en ` .
@@ -75,6 +97,20 @@ Rendered as: `src={`${import.meta.env.BASE_URL}${speaker.image}`}`.
7597
7698SVGs imported as React components via svgr: ` import Logo from "../../assets/logo.svg?react" ` .
7799
100+ #### Speaker & organizer photos
101+
102+ Square ** 400×400** , ` .jpg ` or ` .png ` . This is the 2026 convention and it is applied consistently — 2024/2025
103+ are a mess (200×200 up to 4418×6627), don't copy them. Filename must match the speaker slug.
104+
105+ Speakers send their photo by email, so it usually lands in ` ~/Downloads ` . Resize and convert in one step:
106+
107+ ``` bash
108+ sips -s format jpeg -s formatOptions 80 -Z 400 ~ /Downloads/photo.png --out public/speakers-2026/hugues-lamy.jpg
109+ ```
110+
111+ ` speakers-{year}/user.png ` is the placeholder for a speaker we have no photo of yet. ` hasPhoto() ` in
112+ ` src/utils/speakerFilters.ts ` uses it to keep those out of the home page showcases.
113+
78114### Speaker URL slugs
79115
80116Generated at navigation time from the ` name ` field: lowercase → NFD decomposition (strip accents) → replace spaces with hyphens. Keep this logic consistent between ` SpeakersPage.tsx ` and ` SpeakerPage.tsx ` .
@@ -95,25 +131,36 @@ Centralized in `src/constants/navlinks.ts` with bilingual names. Do not hardcode
95131## Adding Content
96132
97133### Speaker (` src/assets/speakers-{year}/{name}.json ` )
134+
98135``` json
99136{
100137 "name" : " Full Name" ,
101138 "bio" : " Bio or null" ,
102139 "position" : " Title or null" ,
103- "image" : " speakers-2025/photo.jpg" ,
104- "time" : " 2025-11-28T09:00:00" ,
140+ "community" : " Montréal Ruby" ,
141+ "image" : " speakers-2026/photo.jpg" ,
142+ "time" : " 2026-11-27T09:00:00" ,
105143 "track" : 1 ,
106144 "title" : " Talk Title" ,
107145 "description" : " Talk description" ,
108146 "github" : " username or null" ,
109147 "linkedin" : " URL or null" ,
110- "website" : " URL or null"
148+ "website" : " URL or null" ,
149+ "videoLink" : " URL"
111150}
112151```
113152
114- Speakers with ` title === "Intro" ` are filtered out from the speakers grid.
153+ ` community ` (partner meetup hosting the session) and ` videoLink ` are optional — omit them rather than
154+ setting ` null ` . Every other field is required.
155+
156+ Multi-paragraph ` bio ` and ` description ` are plain strings with ` \n\n ` between paragraphs — no markdown.
157+
158+ ` hasAnnouncedTalk() ` in ` src/utils/speakerFilters.ts ` keeps a session out of the speakers grid when ` name `
159+ is empty, ` title ` is empty, or ` title === "Intro" ` . Such entries still show on the schedule — that is how
160+ unannounced slots (keynote, partner meetups) are placeholdered.
115161
116162### Sponsor (` src/assets/sponsors/{name}.json ` )
163+
117164``` json
118165{
119166 "name" : " Company" ,
@@ -126,6 +173,7 @@ Speakers with `title === "Intro"` are filtered out from the speakers grid.
126173```
127174
128175### Organizer (` src/assets/organizers/{name}.json ` )
176+
129177``` json
130178{
131179 "name" : " Full Name" ,
@@ -139,17 +187,17 @@ Speakers with `title === "Intro"` are filtered out from the speakers grid.
139187
140188## Routing
141189
142- | Path | Page | Layout |
143- | ---| ---| ---|
144- | ` / ` | HomePage | Own Navbar + Footer |
145- | ` /about ` | AboutPage | Shared Layout |
146- | ` /venue ` | VenuePage | Shared Layout |
147- | ` /schedule ` | SchedulePage | Shared Layout |
148- | ` /speakers/:year ` | SpeakersPage | Shared Layout |
149- | ` /speaker/:name ` | SpeakerPage | Shared Layout |
150- | ` /sponsors ` | SponsorsPage | Shared Layout |
151- | ` /transparency ` | TransparencyPage | Shared Layout |
152- | ` /conduct ` | ConductPage | Shared Layout |
190+ | Path | Page | Layout |
191+ | ----------------- | ---------------- | ------------------- |
192+ | ` / ` | HomePage | Own Navbar + Footer |
193+ | ` /about ` | AboutPage | Shared Layout |
194+ | ` /venue ` | VenuePage | Shared Layout |
195+ | ` /schedule ` | SchedulePage | Shared Layout |
196+ | ` /speakers/:year ` | SpeakersPage | Shared Layout |
197+ | ` /speaker/:name ` | SpeakerPage | Shared Layout |
198+ | ` /sponsors ` | SponsorsPage | Shared Layout |
199+ | ` /transparency ` | TransparencyPage | Shared Layout |
200+ | ` /conduct ` | ConductPage | Shared Layout |
153201
154202` Layout ` uses ` <Outlet /> ` and derives the page title from ` navLinks ` based on the current path.
155203
0 commit comments