Uh oh!
There was an error while loading. Please reload this page.
Toronto 2026 election: SEO guide pages, verified voting facts, daylight hero - #74
Conversation
Fact-checked every claim on /toronto/vote/2026/how-to-vote against the City Clerk's own pages. The calendar was exact; four things were not. - Step one asserted "all four conditions" while listing three. It now names the fourth, "not prohibited from voting under any law". - Proxy voting was missing entirely, and the page claimed mail-in was "the only one with an application deadline". Toronto has four ways to vote and two of them have deadlines: the proxy appointment form must be certified by the City Clerk up to 4:30 p.m. on election day, and on the day itself only at City Hall. Adds a proxy FAQ and links the City's voting-options page. - The accepted-ID list read "any document issued by the government of Canada, Ontario, or a municipality", which invites a reader to bring a passport. The City lists passports as *not* acceptable, since they carry no address. Tightened to "an Ontario municipality ... as long as it shows your Toronto address" and added a myth box saying so outright — this is the likeliest way the page got someone turned away at the polls. - The steps were static, so the page kept telling readers to apply for a mail-in package after applications closed. VOTING_STEPS becomes getVotingSteps(now), with the phase boundaries derived from ./key-dates rather than retyped, so they cannot drift from the calendar the countdown page uses. Five phases, from before-mail-deadline through to over. revalidate drops to hourly, because two of the boundaries land at 4:30 p.m. and 7 p.m. and a daily one would leave the wrong copy up for most of a day. Also folds in the EI/OAS/CPP benefit statements the City lists under accepted ID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page answers a date query and then left the reader nowhere to go. It now carries 40 internal links where it carried three. - A prose line under the hero CTA, for the majority of readers who already know the date: the mayoral field, their ward's council race, and the how-to-vote guide. Text rather than buttons, so MyVote stays the only solid call to action in the hero. - A new #ballot section above the calendar table, so a reader who bounces after the countdown has still passed a link to the races. It carries the mayoral race, the ward finder, and a crawlable list of all 25 wards linking to their own pages with live candidate counts. - Optional CTAs under three FAQ answers. Deliberately not folded into the JSON-LD answer text: a schema answer that reads "click here" is worse than one that doesn't, and the markup gets stripped anyway. - Four links in the closing CTA instead of three, with the mayoral one promoted to the solid variant. The page is now async and reads the same roster the landing and ward pages use, so its counts cannot disagree with the pages it links to, and it falls back to the local roster when the API is unreachable. One trap worth recording: view.raceCount and view.candidateCount span *every* race, trustees included, so quoting them as "mayor and council" is wrong — it read "362 candidates across all 55 mayor and council races". The narrower figure is derived instead, one mayoral race plus one per ward. Summing ward.count is safe in Toronto because each ward elects its own councillor; in Brampton one race covers two wards and the same sum would double-count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hero photograph now matches the sky over the city it is a picture of: the existing blue-hour frame at night, a new daylight frame by day, with a subtle sun/moon toggle in the corner to override it. Which one you land on comes from the sun's actual elevation over Toronto (src/lib/daylight.ts, NOAA low-precision solar equations) rather than fixed clock hours — a 7 a.m.–7 p.m. window shows a sunlit skyline at 6.30 p.m. in December, an hour and a half after dark. It works off the UTC instant plus a longitude, so there is no timezone lookup and no DST edge case. Validated against published sunrise and sunset: exact to the minute at both solstices and the March equinox, worst case four minutes elsewhere. Toronto's daylight, not the reader's — someone opening this from Vancouver at 5 p.m. their time is looking at Toronto at 8 p.m. The scrim and every type colour move with the photograph, because light copy over a dark wash reads as evening, dark copy over a pale wash reads as afternoon, and either one on the wrong picture is illegible. The day scrim is the heavier of the two on purpose: composited and measured on the real frame, the darkest pixels behind the centred type sit at 5% grey, and the veil has to lift the band to 152/255 before charcoal clears WCAG AA on the worst of them. It now does, at 5.2:1. The day lead is full-strength charcoal rather than a tint for the same reason — at 90% it measured 4.4:1, just under AA. Only the photograph the reader landed on is in the DOM to begin with; the hero is the LCP element and mounting both would put a second full-bleed image on the critical path for a toggle most people never touch. The other mounts on first use and then stays, so later switches cross-fade. Both scrims are mounted from the start, because gradient utilities cannot transition into one another. The mode is decided on the server and handed to HeroSurface as its initial state, so nothing swaps under the reader after hydration. The override is deliberately ephemeral: automatic daylight is the intended default and persisting a manual choice would quietly fight it. Also carries the rest of the in-progress homepage redesign that was sitting uncommitted in page.tsx: the 25-ward outline map on the candidate-tracker card, trimmed card copy, card focus rings, and the HeroEmailCapture -> EmailCapture rename that gave the form its light/dark tone prop — which is what lets it invert with the hero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Greptile SummaryThe PR adds Toronto 2026 election guides, date-aware voting instructions, expanded candidate discovery links, and a daylight-responsive Toronto homepage hero.
Confidence Score: 4/5The PR is not yet safe to merge because the vote-by-mail guide still omits proxy voting from its explicit comparison of available methods. The follow-up fixes the three previously reported locations, but the coordinated voting guidance remains materially inconsistent: the vote-by-mail page still tells readers about only three methods, which can conceal the proxy option from voters unable to attend personally. Files Needing Attention: src/app/toronto/vote/2026/vote-by-mail/page.tsx
|
| Filename | Overview |
|---|---|
| src/app/toronto/vote/2026/vote-by-mail/page.tsx | The public method-comparison section remains inconsistent with the four-option guidance because it omits proxy voting. |
| src/app/toronto/vote/2026/when-is-the-election/page.tsx | The prior proxy-voting omission is corrected by explicitly describing the fourth method and its certification deadline. |
| src/app/toronto/vote/2026/how-to-vote/VotingSteps.tsx | Client-side phase recalculation removes the previously reported hourly stale-action window for JavaScript-enabled readers. |
| src/app/toronto/vote/2026/voter-guide.ts | Phase-aware copy now preserves the mail-ballot return deadline until its actual October 14 cutoff. |
| src/app/toronto/page.tsx | The homepage redesign adds daylight-aware hero presentation and uses only trusted internal values at its changed navigation links. |
| src/lib/daylight.ts | Adds server-side solar-elevation calculation used to select the Toronto hero's initial day or night state. |
| src/app/sitemap.ts | Adds public Toronto election guides and ward routes while retaining redirect and duplicate filtering. |
Prompt To Fix All With AI
### Issue 1
src/app/toronto/vote/2026/vote-by-mail/page.tsx:360-363
**Proxy voting remains omitted**
When a voter who cannot attend a voting place reads this comparison, it lists only mail, advance voting, and election-day voting, omitting the proxy option and causing the reader to conclude incorrectly that no usable voting method remains.
```suggestion <p className="font-serif text-[1.15rem] leading-[1.45] max-w-[34ch] mb-6"> Mail is one of four ways to vote. There is also six days of advance voting, election day itself, and voting by proxy. </p>```---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (7): Last reviewed commit: "Address review: proxy voting, stale step..." | Re-trigger Greptile
Uh oh!
There was an error while loading. Please reload this page.
| /* Hourly, matching the key-dates page. Nothing here counts down, but the | ||
| "pick how you want to vote" step changes as the mail-in, advance-voting and | ||
| online-registration deadlines pass — and two of those land at 4:30 p.m. and | ||
| 7 p.m., so a daily revalidate would leave the wrong copy up for most of a | ||
| day. */ | ||
| export const revalidate = 3600; |
There was a problem hiding this comment.
When a cached page is served shortly after the September 24, 4:30 p.m. deadline, getVotingSteps() still uses the phase captured during the prior hourly regeneration and displays the “Apply to vote by mail” action. This directs voters to an unavailable application path after applications have closed.
Knowledge Base Used:Municipal election engagement
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/toronto/vote/2026/how-to-vote/page.tsx
Line: 16-21
Comment:
**ISR retains expired actions**
When a cached page is served shortly after the September 24, 4:30 p.m. deadline, `getVotingSteps()` still uses the phase captured during the prior hourly regeneration and displays the “Apply to vote by mail” action. This directs voters to an unavailable application path after applications have closed.
**Knowledge Base Used:**[Municipal election engagement](https://app.greptile.com/buildcanada/-/custom-context/knowledge-base/buildcanada/tradingpost/-/docs/municipal-elections.md)---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.The questions are written but the results are not live yet, so the card
sells being told when they are instead of linking to the page. The CTA
becomes "Compare your view to the candidates" and opens the subscribe
modal under its own title, "Be the first to know when we launch" —
everything below the title is the weekly digest modal unchanged, since
that is the list people would be joining.
The modal title is now overridable: openModal takes an optional
headline, and closeModal clears it, or the next opener inherits a
promise that was never made to them. Every existing caller is
untouched and still gets the default.
Signups from here report source "survey-interest" rather than
borrowing "inline", so the funnel stays distinct from the hero's email
capture in PostHog and in HubSpot's placement field. The source union
now lives in the store as SubscribeSource instead of being spelled out
in three places.
The card could not stay a single anchor: its action is a modal, and a
<button> may only contain phrasing content while these cards hold a
heading and a list. So the navigating card is still one anchor, and
this one becomes a plain container with a real button at its foot
stretched over the whole card by an ::after overlay. Both keep one hit
area and one accessible name.
Nothing on /toronto links to the survey page now. That page is already
noindex and excluded from the sitemap, so this orphans nothing that
was indexed; ElectionCard carries a note to swap href={SURVEY_PATH}
back in at launch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>ExitIntentHandler already did the work — it opens the subscribe modal when the pointer crosses the top edge of the viewport, which is the gesture that ends at the tab bar or the close button — but the root homepage was the only thing mounting it, so none of the Toronto pages ever offered it. Mounts it on /toronto's layout, which covers the memos, the election tracker, the ward pages and the guides. On the layout rather than the pages so the listener survives client-side navigation: someone who reads three ward pages is asked at most once, not once per page. The two gates are unchanged and are the whole feature, so the rule is now a pure exported function with the reasoning written down beside it: never ask someone who has already subscribed, and never ask someone who has already declined until that dismissal is a week old. Worth knowing that `dismissed` is set by SubscribeModal on any close without a signup, so opening the modal from the navbar and closing it counts as having been asked. Deliberately not listening for the tab actually closing. beforeunload cannot render custom UI and Cmd/Ctrl+W is not interceptable, so the mouse-toward-the-close-button gesture is the only honest signal; a visibilitychange listener would have fired on an innocent Cmd+Tab. mouseleave never fires on touch, which makes this desktop-only by nature. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
/when-is-the-election was answering four questions at once. It carried three live countdown cards, a three-column explainer of every voting method, and eight FAQs covering early voting, mail deadlines, eligibility and ID — which buried the one fact most of its readers came for, which is the date of election day. It is now the election-day countdown and the full calendar, and the two periods with real detail of their own have their own pages: /toronto/vote/2026/advance-voting — the six days, the hours, why the places differ from election day's, registering on arrival, and the accessibility services /toronto/vote/2026/vote-by-mail — both deadlines, how to request a package, the ward drop boxes, and the change-of-mind rule The hub keeps a three-up signpost row in place of the countdown cards, so a reader who needs one of those still finds it in the same spot. Visible copy on the page drops 36%. The mail page's hero counts down whichever deadline is still live: the application until 4:30 p.m. on September 24, the return deadline after that. Counting down a deadline that has already gone is worse than counting down nothing, and its CTA swaps with it. Verified at both boundaries. Both new pages revalidate hourly, like the hub, since their countdowns are baked into the HTML for crawlers and no-JS readers. Two things this fixes as a side effect: - No FAQ question now appears on more than one of the four pages. Six had been competing, two of them near word-for-word duplicates of /how-to-vote's. 26 questions, each owned by exactly one page. - The VotingEvent markup was being emitted for all four periods on the key-dates page. Each event is now described once, on the page that owns it, instead of two URLs bidding for the same result. Also drops a claim I could not source: the advance-voting blurb said you may vote at any advance place in your ward. toronto.ca does not say that anywhere I could find — it says your assigned place is on your voter information card and in MyVote — so the new pages say that instead. The "no reason required" half is standard and stays. Adds PeriodCountdown, since the hero, both new pages and the mail page's two deadline cards all needed the same server-reads-once, client-ticks-forward dance, and getting `targetInstant ?? 0` wrong in one of them would have shipped a stopped clock. Sitemap, OG images and the landing page's guide links all pick up the two new routes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three running countdowns on one page, and the two smaller ones were competing with the hero's — which is the only one a reader needs, since it already shows whichever of the two deadlines is still live. The deadline cards now state their dates and say what happens if you miss them, which is what they were for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three P1s from the review, all of them ways the pages could tell a voter something that costs them their ballot. Proxy voting was missing wherever the pages compared methods. voter-guide already had it at four; /when-is-the-election, the how-to-vote description and OG subtitle, and the advance-voting cross-link all still said three. The key-dates section gets the fourth as a line under the grid rather than a card — proxy has no date of its own and no page of ours to link to — and the copy that counted methods now counts four. The steps could go stale for an hour. The page is ISR-cached hourly and the steps are phase-dependent, so a cached copy could keep offering the mail-in application after 4:30 p.m. on Sept 24, or online registration after 7 p.m. on Oct 11 — the hour when people are actually looking. VotingSteps re-derives from the reader's own clock on mount and every minute after, same shape as LiveCountdown: server props verbatim on first render, effect takes over. Crawlers still get the server's copy, never more than an hour old. The mail-in return deadline vanished between Oct 11 at 7 p.m. and Oct 14 at noon. It sits three days inside `election-day-only`, so the phase copy dropped it while there were still voters holding a package. waysToVoteBody takes `now` and keeps the sentence until the deadline actually passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| <p className="font-serif text-[1.15rem] leading-[1.45] max-w-[34ch] mb-6"> | ||
| Mail is one of the ways. There is also six days of advance | ||
| voting, and election day itself. | ||
| </p> |
There was a problem hiding this comment.
When a voter who cannot attend a voting place reads this comparison, it lists only mail, advance voting, and election-day voting, omitting the proxy option and causing the reader to conclude incorrectly that no usable voting method remains.
| <pclassName="font-serif text-[1.15rem] leading-[1.45] max-w-[34ch] mb-6"> | |
| Mailisoneoftheways.Thereisalsosixdaysofadvance | |
| voting,andelectiondayitself. | |
| </p> | |
| <pclassName="font-serif text-[1.15rem] leading-[1.45] max-w-[34ch] mb-6"> | |
| Mailisoneoffourwaystovote.Thereisalsosixdaysof | |
| advancevoting,electiondayitself,andvotingbyproxy. | |
| </p> |
Knowledge Base Used:Municipal election engagement
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/toronto/vote/2026/vote-by-mail/page.tsx
Line: 360-363
Comment:
**Proxy voting remains omitted**
When a voter who cannot attend a voting place reads this comparison, it lists only mail, advance voting, and election-day voting, omitting the proxy option and causing the reader to conclude incorrectly that no usable voting method remains.
```suggestion <p className="font-serif text-[1.15rem] leading-[1.45] max-w-[34ch] mb-6"> Mail is one of four ways to vote. There is also six days of advance voting, election day itself, and voting by proxy. </p>```**Knowledge Base Used:**[Municipal election engagement](https://app.greptile.com/buildcanada/-/custom-context/knowledge-base/buildcanada/tradingpost/-/docs/municipal-elections.md)---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Uh oh!
There was an error while loading. Please reload this page.
Two new SEO landing pages for the Toronto 2026 election, a fact-check pass over the voting information on them, a lot more internal linking into the candidate tracker, and a hero that follows the city's actual daylight.
New pages
/toronto/vote/2026/when-is-the-election— owns the "when" queries. Live countdowns to advance voting, the vote-by-mail deadlines and election day, the full calendar as a table, and FAQPage + VotingEvent JSON-LD./toronto/vote/2026/how-to-vote— owns "who, what and how". Eligibility, the five steps, what ID to bring, what's on the ballot. Its FAQs are deliberately disjoint from the other page's: both target the same election, so answering the same questions would have them competing in the index instead of covering the query space between them.Supporting bits:
LiveCountdown,src/lib/elections/dates.ts, anEvent/VotingEventschema generator, sitemap entries, and OG images for both pages.Voting facts, verified
Checked every claim on
/how-to-voteagainst the City Clerk's own pages — key dates, who-can-vote, identification, voters' list, mail-in voting, voting options. The calendar was exact. Election day, advance voting, both mail-in deadlines, the voters'-list cutoff and nomination day all match toronto.ca to the minute, and every weekday label is right for 2026. Four other things were not:getVotingSteps(now), five phases, boundaries derived fromkey-dates.ts. Hourly revalidate, since two land at 4:30 p.m. and 7 p.m.The passport one is the likeliest way the page got somebody turned away at the polls.
More paths into the candidate tracker
/when-is-the-electionanswered a date query and then left the reader nowhere to go. 40 internal links now, up from three: a prose line under the hero CTA, a new#ballotsection above the calendar (mayoral race, ward finder, and a crawlable list of all 25 wards with live candidate counts), CTAs under three FAQ answers, and four links in the closing CTA.The page reads the same roster the landing and ward pages use, so its counts can't disagree with the pages it links to, and it falls back to the local roster when the API is unreachable.
Daylight hero
The
/torontohero now matches the sky over the city it's a picture of — the existing blue-hour frame at night, a new daylight frame by day, with a subtle sun/moon toggle in the corner to override it.Which one you land on comes from the sun's actual elevation over Toronto (
src/lib/daylight.ts, NOAA solar equations) rather than fixed hours — a 7am–7pm window shows a sunlit skyline at 6:30pm in December, 90 minutes after dark. Validated against published sunrise/sunset: exact to the minute at both solstices and the March equinox, worst case four minutes elsewhere. It uses Toronto's daylight, not the reader's.The scrim and every type colour move with the photograph. The day scrim is heavier on purpose: composited and measured on the real frame, the darkest pixels behind the centred type sit at 5% grey, so the veil has to lift the band to 152/255 before charcoal clears WCAG AA — it now does, at 5.2:1. The day lead is full-strength charcoal rather than a tint for the same reason; at 90% it measured 4.4:1, just under AA.
Only the photograph you land on is in the DOM initially — the hero is the LCP element, and mounting both would put a second full-bleed image on the critical path for a toggle most people never touch. The other mounts on first use and stays, so later switches cross-fade. The mode is decided server-side and handed to
HeroSurfaceas initial state, so nothing swaps under the reader after hydration.The last commit also carries the rest of the in-progress homepage redesign that was sitting uncommitted: the 25-ward outline map on the tracker card, trimmed card copy, focus rings, and the
HeroEmailCapture→EmailCapturerename that gave the form its light/darktoneprop — which is what lets it invert with the hero.Notes for review
localStorage. Automatic daylight is the intended default and persisting a manual choice would quietly fight it. Easy to change if you'd rather it stuck.next buildlocally — it compiles clean, then prerendering dies onECONNREFUSEDreaching the API (/abouton one run,/sitemap.xmlon the next). Unrelated to these changes; the elections layer degrades to its local roster as designed.tsc --noEmitandeslintare clean, and every page was verified rendering against the dev server. Worth a look at the Vercel preview build.🤖 Generated with Claude Code