fix(training): fetch TeSS listings at build time - #332
Merged
Conversation
All three listings on /training were driven by the TeSS browser widget, and all three had been failing silently. The widget issues its XHRs without an Accept header, so TeSS answers with an HTML 403 that carries no CORS headers, and the browser reports the result as a CORS violation. It also tries to set a User-Agent, which browsers forbid. Nothing catches any of this, so the page threw three uncaught TypeErrors and rendered no listings at all. The static placeholder underneath made it worse. "No upcoming training events at the moment." was our own markup waiting to be replaced, so the page claimed there was nothing on instead of admitting it had failed to ask. TeSS was serving ten Norwegian materials and ten past events the whole time. The fetch now happens during the build, where an Accept header is enough to get JSON back and CORS never comes into it. The third-party script and stylesheet are gone, and so are the 123 lines of overrides that existed to restyle markup we now emit ourselves. TeSS is unreliable, around one request in three came back 5xx while I was testing, so this retries and then degrades. It also separates an empty listing from an unreachable one. Repeating the old bug's habit of showing a plausible empty state over a dead feed would have defeated the point, so a failed fetch says so and links out to TeSS. Drops the unused BASE constant while the file is open.
There was a problem hiding this comment.
Pull request overview
This PR fixes /training TeSS listings failing at runtime by moving TeSS data fetching to build time, replacing the browser widget integration with server-side fetch/retry logic and first-party-rendered markup (including an explicit “unavailable” state when TeSS can’t be reached).
Changes:
- Fetch upcoming events, past events, and materials during the Astro build and render them as static HTML.
- Add
src/lib/tess.tsto handle TeSS API querying, retries/backoff, and event date/place formatting. - Add a reusable “TeSS unavailable” UI component and keep a client-side materials filter.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/pages/training/index.astro | Replaces TeSS widget runtime integration with build-time fetched lists + client-side materials filter. |
| src/lib/tess.ts | Adds TeSS API fetch/retry utilities and formatting helpers used by the training page. |
| src/components/tess-unavailable.astro | Adds a dedicated unavailable-state component for unreachable TeSS listings. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Event and material URLs come from a feed we do not control, and a build-time render writes them into static HTML, so a javascript: URL would ship and then sit there until somebody rebuilt. eventLink and the new materialLink now parse the URL and fall back to a TeSS address we construct ourselves whenever the scheme is anything but http or https. test-pages.mjs grew a matching check over the built output, because the helper is only one of the routes a URL can take into an href. It allows http, https, mailto and tel, which covers everything the site emits today, and fails the run on anything else. Confirmed by injecting a javascript: href into dist and watching the check catch it. Raised in review on #332.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/traininghas three listings on it. All three have been failing silently on production, showing nothing.What was broken
The listings were driven by the TeSS browser widget (
elixirtess.github.io/TeSS_widgets). It fires its XHRs without anAcceptheader, so TeSS answers with an HTML 403 that carries no CORS headers, and the browser surfaces that as a CORS violation. Add the header and the identical URL returns200withaccess-control-allow-origin: *, so CORS was never really the issue. The widget also tries to set aUser-Agent, which browsers forbid, which suggests it was written for a server-side context and dropped into a page.Nothing catches the failure, so the page threw three uncaught
Cannot read properties of null (reading 'data')and rendered no listings.The static placeholder underneath is what made this hard to notice. "No upcoming training events at the moment." is our own markup at the old
index.astro:153, sitting there for the widget to replace. It never got replaced, so the page has been asserting there is nothing on rather than admitting it failed to ask. TeSS was serving ten Norwegian materials and ten past events throughout.What changed
The three fetches moved into the build. An
Accept: application/jsonheader is all TeSS wants, and CORS does not apply outside a browser. Gone: the widget script, its stylesheet, and 123 lines of.tess-container .tess-widgetoverrides that existed only to restyle markup we now emit ourselves.New files are
src/lib/tess.ts(fetch, retry, date and place formatting) andsrc/components/tess-unavailable.astro.The materials search box survives as a client-side filter over the fetched list, with
aria-liveon the empty state.Handling the fact that TeSS falls over
Roughly one request in three returned 5xx while I was testing, and successful ones took 11 to 19 seconds. So each endpoint gets five attempts with a rising backoff.
More importantly, the page now distinguishes an empty listing from an unreachable one. Reproducing the original bug's habit of showing a plausible empty state over a dead feed would have defeated the point of the PR. A failed fetch renders "This listing could not be loaded from TeSS when the site was last built" plus a link to the matching TeSS page, and logs
[tess] events failed after 5 attempts (HTTP 522)to stderr so it is visible in the build output.This is not hypothetical. One of my verification builds hit exactly that: TeSS 522'd four times running on both events endpoints. That run is why the reachable/empty split exists.
Verification
pnpm build,test:slugsandtest:pagesgreen, 303 pages,astro checkat 0 errors and 0 warnings.Driven in a browser against the built output: 10 materials, 10 past events, 0 upcoming (TeSS genuinely returns
[]for upcoming Norwegian events right now), no console or page errors where there were three. The filter was exercised for a match, a non-match, and a clear.The outage path was tested by pointing the helper at an unresolvable host: the build still succeeds, all three sections render the unavailable state with their TeSS link, and the warnings appear on stderr.
Notes
Venue is deliberately left out of the one-line summary. TeSS stores a street address there and frequently a doubled one (
'Moltke Moes vei, Moltke Moes vei'), so the line shows city and country and the linked event page carries the address.The tradeoff is that listings refresh on deploy rather than on page load. For a training catalogue that seems right, and it is what makes the page immune to TeSS being down when someone visits.
Also drops an unused
BASEconstant that was already dead onmain.