Skip to content

Repository files navigation

A statically generated site for WXYC. Built using the frontend framework React, NextJs as a static site generator and TinaCMS as a Git-based content management system. Styled using TailwindCSS, with help from the MUI Joy UI component library on implementing breadcrumbs and Headless UI on implementing dropdown menus. Deployed to Github Pages.

Supports content management for the radio station's blog and for an archive of the radio station's specialty shows and live events. Very much a work in progress!

Visit live site


image

[the development of this site is brought to you by the easily accessible assortment of photos of Adrianne Lenker that live on my desktop]

Playlist archive (/playlists/archive)

Public, week-at-a-time browse of every show WXYC has logged, back to at least November 2004. Successor to wxyc.info/playlists/radioWeek, which went dark at the 2026-09-07 tubafrenzy cutover (WXYC/wiki#93).

  • Data source: Backend-Service GET /flowsheet/range?start=&end= (epoch milliseconds, half-open [start, end), 8-day ceiling). Contract lives in wxyc-shared/api.yaml. Override the origin at build time with NEXT_PUBLIC_WXYC_API_URL; it defaults to https://api.wxyc.org.
  • Client-side only. This site is a static export, so there is no SSR and no getStaticPaths over a 2.6-million-row table. The week lives in ?week=YYYY-MM-DD (always a Monday) so a week is linkable, and the fetch happens after hydration.
  • Weeks and days are Eastern, not UTC and not browser-local — see lib/easternTime.js. Day and week bounds are computed by calendar arithmetic rather than by adding fixed millisecond offsets, because the spring-forward week is 167 hours and the fall-back week is 169.
  • Playlists are collapsed by default. A week is 2,300–2,800 entries and 470–640 KB gzipped, so each show is a <details>: the schedule is always visible and the rows only get laid out when opened. Note that the rows are still built<details> skips layout, not DOM construction. Deferring construction to first open is the next lever if the page ever feels slow on a phone.
  • Entries are ordered by play_order, not by arrival. The endpoint returns rows in add_time order, and the two disagree whenever a DJ enters a row after the fact — 88 times across 36 of the 54 shows in a sampled production week. Ordering by arrival strands retroactively-added hour breakpoints in the middle of a later hour.
  • Past weeks are cached in memory (lib/weekCache.js). The endpoint sends no Cache-Control, so without this every Previous/Next click and every browser Back re-downloads half a megabyte. The week in progress is deliberately never cached.
  • The requested week is clamped to [2004-11-01, current week]. ?week= accepts anything, and <input type="date"> reports every keystroke of a typed year (editing to 2026 emits 0002, 0020, 0202 first), so without a clamp each of those becomes a live range query against a 2.6-million-row table.

Live playlist (/playlist)

Public view of the most recent flowsheet entries, refreshing while the tab stays open. Successor to wxyc.info/playlists/recent, which goes dark at the 2026-09-07 tubafrenzy cutover (WXYC/wiki#93).

  • Data source: Backend-Service GET /flowsheet?page=0&limit=50. Distinct from the archive's /flowsheet/range: this endpoint returns one flat entries array plus pagination metadata and the currently on-air DJ, rather than a separate shows array, so there is no grouping step. Contract lives in wxyc-shared/api.yaml. Override the origin at build time with NEXT_PUBLIC_WXYC_API_URL; it defaults to https://api.wxyc.org.
  • Client-side only, for the same reason as the archive: a static export has no SSR and no server to poll from, so the fetch happens in the browser, on an interval, for as long as the tab stays open.
  • Entries are ordered by show_id then play_order, both descending, then id descending — most recent show first, and within a show by the DJ's stated air order rather than the order the endpoint returned them in (insertion order). The endpoint returns rows newest-inserted-first, and a DJ who enters a row after the fact gets a row whose id says "just now" but whose play_order says otherwise; rendering in insertion order strands it in the wrong place. The id tie-break exists because play_order can legitimately collide — the tubafrenzy webhook and the dj-site live-insert path assign it independently with no per-show UNIQUE constraint — matching how Backend's own getEntriesByShow breaks the same tie. See compareEntriesByAirOrderDesc in lib/flowsheetRange.js, shared with the archive page's own play_order rule.
  • A failed poll keeps the last good playlist on screen rather than replacing it with an error: the table is still true, just stale. A "Last updated HH:MM — couldn't refresh" notice and a Retry button appear alongside it so the page is never silently frozen; only a failure on the very first load (nothing to show yet) replaces the page with a full error state.
  • Polling pauses while the tab is hidden — checked both at mount and on every subsequent visibility change, so a tab opened directly in the background never fetches at all until it is actually shown — and catches up with one fetch when it becomes visible again, rather than continuing to poll a tab nobody is looking at. The response is ~51 KB with Cache-Control: no-cache, and a tab left open for a workday would otherwise issue roughly 1,440 requests for ~73 MB.
  • Each poll aborts any still-in-flight one before starting, so a slow earlier response landing after a faster later one cannot overwrite fresher data with stale.

Airplay search (/airplay-search)

Public search over WXYC's full airplay history, back to at least November 2004. Successor to wxyc.info/playlists/searchPlaylists, which went dark at the 2026-09-07 tubafrenzy cutover (WXYC/wiki#93).

  • Data source: Backend-Service GET /flowsheet/search?q=&page=&limit=. Contract lives in wxyc-shared/api.yaml. An empty or omitted q is a supported request, not an edge case — it is what makes the backend serve its most-recent-tracks default, which is this page's landing view. Same NEXT_PUBLIC_WXYC_API_URL build-time override as the archive page.
  • Client-side only, for the same reason as the archive page: this site is a static export, so there is no SSR to fetch behind. The query and page number live in component state rather than the URL.
  • total is a capped sentinel, not a count. The backend's COUNT_CAP is 10000; any query matching more rows — including the empty-query landing view, against the ~2.6-million-row table — comes back with total pinned at exactly 10001. Render it raw and the default state of the page prints a false number to every visitor. formatSearchTotal shows anything past the cap as an open-ended 10,000+ instead.
  • Reachable pages are clamped well short of where the endpoint actually fails (MAX_REACHABLE_PAGE, currently 99). Deep OFFSET pages on this endpoint approach and can exceed the backend's 5-second statement timeout, and the page where that happens is not a fixed number — it moves with database load, so no constant can safely sit right at the edge. The clamp is set at roughly a quarter of the smallest offset ever observed to survive, trading away some reachable depth for headroom against that drift, rather than being tuned close to a boundary that has already been seen to move. The actual fix is switching to the endpoint's cursor-paging mode, which is O(limit) instead of O(page × limit) and has no such cliff; raising this constant instead of making that switch just moves the same failure further out.

DNS & hosting (do not "fix" the Pages domain warning)

wxyc.org and www.wxyc.org are proxied through Cloudflare (orange-cloud) rather than pointing their DNS records directly at GitHub Pages. Cloudflare forwards the Host header to GitHub Pages as the origin, so the site is still built and served by this repo's Pages deploy exactly as before — Cloudflare just sits in front of it (this is what lets us attach edge Workers to the apex).

Because the public A/AAAA records no longer resolve to GitHub's Pages IPs (they resolve to Cloudflare's edge), repo Settings → Pages will show a warning that the custom domain's DNS does not point at GitHub Pages. This warning is expected and cosmetic — the site works. Do not change the DNS records back to GitHub's IPs, and do not clear the custom-domain field in Settings → Pages (the custom domain lives only there; there is no CNAME file in the repo). Reverting either would break the Cloudflare proxy in front of the apex. If you need to take Cloudflare out of the path, toggle the apex + www records from orange (Proxied) back to gray (DNS only) in the Cloudflare dashboard.

TLS & certificates (do not set SSL/TLS mode to Full (strict))

There are two certificates in front of this site. The edge certificate is Cloudflare's (Google Trust Services) and is what browsers actually see; Cloudflare renews it automatically and it has never been a problem. The origin certificate is the Let's Encrypt certificate GitHub Pages manages for the custom domain, presented to Cloudflare when it fetches from the origin. Since the proxy went in front of the apex the origin certificate is invisible to browsers — read it by connecting to a Pages IP directly rather than to wxyc.org: echo | openssl s_client -servername wxyc.org -connect 185.199.108.153:443 2>/dev/null | openssl x509 -noout -issuer -dates.

The origin certificate's renewal is currently broken. GET /repos/WXYC/website/pages reports https_certificate.state: "bad_authz" — "The ACME authorization is in a bad state. We need to start over." The ACME challenge path is not the cause: /.well-known/acme-challenge/* reaches the Pages origin untouched through the proxy for both wxyc.org and www.wxyc.org, and there are no CAA records on the domain. Only GitHub can clear this state, and the WXYC org is on GitHub Free, whose support form offers no Pages category — so the route is GitHub Community Discussions. Full investigation in #209.

Because of that, the Cloudflare SSL/TLS mode must stay on Full (non-strict), not Full (strict). Full (strict) validates the origin certificate, so when the stuck certificate expires the site returns 526 and goes completely offline; under plain Full, Cloudflare still encrypts to the origin but does not validate it, so an expired origin certificate is harmless. This is what keeps the 2026-09-19 expiry from being an outage. Restore Full (strict) only once https_certificate.state reads approved and a certificate with a new serial is confirmed on the origin. #209's original preflight instruction to use Full (strict) is superseded by this note.

Do not try to fix the certificate by clearing and re-adding the custom domain. This was tested twice — with an immediate re-add, and with a 20-minute gap — and GitHub restored the identical bad_authz record both times, so it does not work at any duration. It is also actively harmful: while the domain is detached, Pages returns 404 for the hostname and GitHub's Fastly layer caches that 404 per path, then keeps serving it well past its own max-age. The only purge available is a new Pages deployment — and since this site normally deploys only when TinaCMS content changes (sometimes a week apart), a poisoned path can stay broken for days. If you must verify something while the domain is detached, probe the origin with a Host: header (curl -k -H 'Host: wxyc.org' https://185.199.108.153/) rather than through Cloudflare, which is what creates the cache entry in the first place.

Two related settings follow from this and should be left alone. Always Use HTTPS is enabled at Cloudflare and is what now serves the http→https redirect, because GitHub's own https_enforced is stuck false and cannot be re-enabled while the certificate is unissued. And www.wxyc.org is redirected to the apex by a Cloudflare Page Rule (www.wxyc.org/*https://wxyc.org/$1, 301) rather than by GitHub's www-to-apex canonicalization, so www no longer touches the Pages origin at all.

Wishlist

  • Setlist component for blog posts
  • Contact form and instagram integrations (implemented in development, need to figure out how to host)
  • Search bar for archive and blog
  • Utilize Tina blocks to make more components of site editable by admin
  • Embed audio player in layout so music can continue as you browse the site

About

Static website for WXYC.org

Resources

Stars

2 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages