Skip to main content
Back to all posts
Sep. 2026 - Present9 min read

Scores typed courtside, on the website before the next game starts

Montreal United runs mentorship and organised sports for youth in Montreal, including two annual basketball tournaments. I took over their bilingual WordPress site in September 2026 and built them a second thing they asked for: a way to run a tournament from a phone at the gym. Divisions, pools, generated games and scores go in on the phone, and the public page picks them up on its own, in English and French. The part I spent the most care on is the part that refuses, because the person using it is standing at a scorer's table with a game finishing behind them.

WordPressDiviWPMLPWABilingualClient workLong-term

Montreal United is a non-profit that runs mentorship and organised sports for youth in Montreal. Two basketball tournaments anchor their year: the Lindsay A. Burnett Invitational and No Politics, Just Ball. I took over the website in September 2026 and have been running it since. It is WordPress on Divi with WPML, which means every page exists twice, once in English and once in French, and the two have to stay in step or the French side quietly rots.

The Montreal United homepage: a black and white basketball going through a hoop, the wordmark, and the line about providing mentorship and development to the youths of Montreal through organized sports.
The live site. A volunteer-run non-profit, two annual tournaments, and every page of it in both English and French.

The half that is not the website

The brief that mattered was not about the site at all. During a tournament, scores exist on paper at a scorer's table and nowhere else until somebody gets home and types them up. Parents in the gym refresh a page that has not changed since Thursday. What the organisation wanted was the ability to update teams and scores from a phone, on the floor, and have the website take care of itself. So the second half of this engagement is a tool: a WordPress plugin with its own database tables and an admin-only REST namespace, paired with a progressive web app that installs from a link. No app store, no review queue, no build step on their side. You open a link, add it to your home screen, and it behaves like an app.

The tournament app on a phone: the L.A.B Invitational banner, a card for the 2026 tournament marked as on the website with its dates and eight divisions, and a form to start a new tournament.
The app's first screen. Each tournament card says plainly whether it is on the website or not, because that is the only thing on this screen with consequences outside the phone.

A tournament is a draft until somebody says otherwise

The person using this is standing at a scorer's table with a game finishing behind them and a parent asking a question. That context decided most of the design. Every tournament starts as a draft, and the public website only ever renders published ones, so nothing anyone does in the app reaches the public site until they press a button that says so. There is a one-tap practice tournament that fills itself with sample games on today's date, and it wears a banner on every screen saying it is not on the website and that you can try anything. Publishing and unpublishing are separate explicit buttons behind a confirmation, and the confirmation warns you when another tournament is already live or when the name looks like a practice. Clearing all scores is undoable. Renaming a team is undoable.

Entering a score on the phone: two team names with 61 and 58 in large boxes, state buttons for scheduled, live, final and forfeit with final selected, and day, time, court and an optional note underneath.
Score entry. Big targets, a state rather than a checkbox, and the practice banner still visible at the top so there is never a question about whether this is the real thing.

Getting a score wrong is normal, so correcting one had to be as cheap as entering one. Reopening a finished game shows a note acknowledging the mistake and lets you change either number, and the saved card updates immediately with the winner in bold. The undo bar sits at the top of the screen rather than in a menu, because the moment you need it is the moment you are least willing to go looking.

The day's games list after saving: the finished game shows 61 to 58 with the winning team in bold, an undo control at the top of the screen, and the remaining games below still waiting for scores.
Saved. The finished game moves to a result with the winner in bold, and undo is one tap away at the top of the screen for as long as it is useful.

Divisions, pools, and the games in between

A tournament here is eight divisions by age and gender, each with its own teams, and the 2026 edition ran eighty-eight games. The app builds that structure rather than asking anyone to type it: add the teams to a division, split them into pools by seed or by hand, and generate the round robin. Standings come out with tiebreakers applied, and the crossover games that follow resolve their seeds from the pool results instead of needing to be entered again. Regenerating is safe, because it is a separate button that tells you what it is about to replace.

A division open on the phone: six teams each tagged with a pool letter and a seed, controls to create a pool or split into pools by seed, and a games section below with a regenerate action.
A division with its teams, pool letters and seeds. Splitting into pools and generating the schedule are two taps, and both are reversible.

The beat that makes it worth it

Here is the whole point of the project in one sentence: a score typed at the scorer's table is on the public website before the next game starts, and nobody did anything to make that happen. The public tournament page refreshes itself, so the parent standing in the gym with a phone, and the grandparent watching from another city, see the same number within the minute. No export, no upload, no volunteer at a laptop that evening.

The public tournament schedule on a desktop browser, showing a completed game reading Wolves 55 to 52 Drop Off in the Thursday listing, which arrived from the phone with no page reload by a person.
The same score, on the public page, arriving on its own. This frame is from a capture where the score went in on a phone and the desktop picked it up without anyone touching it.

Both languages, or it does not ship

This is Montreal, the organisation serves families in both languages, and a French page that lags the English one is worse than no French page because it looks maintained and is not. So the tournament views are bilingual at the source: the same schedule, divisions, pools and standings render at /tournament/ and /fr/tournoi/, with the division names using the organisation's own French terms rather than a machine's guess at them. The validation suite asserts the tournament dates appear on the home, contact and tournament pages in both languages, so an edit that updates only the English side fails before it reaches anybody.

The same tournament page in French at /fr/tournoi/, with the schedule grouped under jeudi 14 mai and vendredi 15 mai, and the division and venue filters translated.
The French side of the same tournament, rendered from the same records. The days, filters and division names are translated; the scores are the identical rows.

The unglamorous half: 2.5 seconds down to about 150 milliseconds

The site runs thirty-five plugins, and an uncached request spent about 2.5 seconds inside WordPress before sending a byte, with WooCommerce alone accounting for roughly 0.8 of that. The tempting move is to start deleting things. The correct move for a volunteer-run organisation that depends on those plugins for registration and payments was to put page caching in front of it and leave the stack alone. I added a warmer that rebuilds every page in batches, saves its progress, re-checks the whole set at the end of a pass, and runs again if something wiped pages mid-run. All twenty-four public pages now serve from cache in about 150 milliseconds.

The public tournament page on desktop: the tournament name and dates, tabs for schedule, divisions and live, filters for division and venue, a print link, and the schedule grouped by day with time, game, division and court.
What the tool produces for the public: schedule by day, divisions with pools and standings, a live tab, filters, and a print view for the people who still want paper at the door.

What was hard

Three things, none of which I would have predicted. The host refuses any request body over one megabyte: Apache answers before PHP runs, so the media endpoint simply cannot take a photo straight off a phone, which is exactly what the client would try first. I measured the boundary, 1024 KB accepted and 1100 KB refused, then wrote a chunked upload that assembles a file from smaller pieces, checks the SHA-256 of the result against the one sent, and only then registers it. Second, WPML translates attachments, so saving a French page silently rewrites every gallery ID to a French twin pointing at the same files, which makes a naive read-back comparison fail on every French page that holds a gallery. Third, the page builder was auto-playing the first video in a tab, unmuted, the instant a visitor opened that tab, and because clicking the tab counts as a user gesture the browser's autoplay policy did not stop it. That one had been live and nobody had reported it.

What I learned

The feature I am proudest of is the one that does nothing. A practice tournament, a draft state, a confirmation that reads the situation and warns you when something else is already live: none of it appears in a feature list and all of it is why somebody will actually use this during a real tournament rather than waiting until they are home. The second lesson is older and I keep relearning it. Everything I shipped here is checked by a script that runs against the live site, now 104 assertions including a round trip performed as the client's own account, because on a bilingual site the failure mode is never a broken page, it is a French page that stopped matching the English one four weeks ago.

What's next

  • The 2027 tournaments, the first ones where the tool runs a live weekend from the opening game rather than being tested against a finished season.
  • Raising the host's one megabyte request limit, so the client can upload a photo straight from a phone without the chunked workaround standing in for it.
  • A rotating gallery of tournament photos on the tournament pages, waiting on the photos rather than on code.
  • Continuing the routine work the site actually lives on: content, tournament dates, registration forms each cycle, WordPress and plugin updates, backups and monitoring.
Related project

Montreal United: bilingual WordPress site for a Montreal youth basketball non-profit, plus a phone tool that puts tournament scores on it live

View the project