Fix eight correctness bugs, then make link previews and SEO work - #1
Merged
Conversation
- load.test.ts "sorts by date descending" asserted a one-element array equals itself sorted; the fixture dir holds a single episode. The ordering contract is already pinned directly by the episodeOrder tie-break test below it. - load.test.ts's tie-break test carried a redundant re-import and a trailing assert.ok(await load()) unrelated to what it tests. - srt.test.ts "the last cue runs five seconds" asserted two substrings of the exact SRT string pinned by the test immediately above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
scripts/migrate.ts converted the historical reactiflux.com transcripts into content/episodes/. That conversion has run; the output is committed and is now the source of truth. Nothing imports the script, and it has no package.json entry. Removes the script, tests/migrate.test.ts, and the seven legacy-*.md fixtures used only by it. src/content/headings.ts and scripts/publish-transcript.ts keep insertHeadings, which publish-transcript still uses; their doc comments no longer point at migrate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
- publish-transcript: fetchDescriptTranscript now rejects a 200 response whose body is under 500 characters, so a degenerate export can no longer canonicalize to "" and wipe the committed transcript before writeFileSync. - ingest: applyFeedItem only assigns `people` when the feed item actually carries <podcast:person> tags, matching the !== undefined guards on its sibling fields, instead of clobbering curated front matter with []. - ingest: parse <itunes:duration> with toSeconds so the RFC-legal HH:MM:SS / MM:SS form is accepted instead of yielding NaN and silently dropping the feed item; the remaining skip path now warns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
- parse.ts: detect the `# Transcript` marker at position 0, not just after a
newline, so a body that opens with it no longer parses to zero sections.
- parse.ts: reject a missing or unparseable front matter `date` at parse time,
naming the episode, instead of propagating "" into `<pubDate>Invalid Date</pubDate>`
and into a mid-loop throw in publish-atproto after records are already written.
- srt.ts: clamp every cue to at least one second and never end it before its
start; 67 zero-length/reversed cues across the 39 episodes are now zero.
- headings.ts: first item to claim a paragraph keeps it, so an out-of-order
outline can no longer overwrite (and drop) a heading already placed.
- time.ts: `toSeconds("")`/blank returns undefined instead of 0, and more than
three colon-separated parts is rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
"Home" and "Episodes" both linked to "/", making Episodes a dead nav item on every page. Point it at /#archive and give the homepage's archive section a matching id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
✅ Deploy Preview for jazzy-clafoutis-b6a109 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
satori + resvg render one 1200x630 PNG per episode into dist/client/og/<slug>.png (plus og/default.png) from the show artwork and a committed latin-subset Inter, so the build stays offline. Wired into postbuild ahead of pagefind; check-build asserts every card exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
Absolute og:image pointing at the generated cards, og:type/url/locale, image dimensions, twitter:card=summary_large_image and theme-color so Discord and Slack render a full-width embed. Episodes also carry article:published_time and best-effort og:audio tags. cardTitle moved to src/content/slug.ts so the episode <title> stops doubling the site name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
The artwork's film grain defeats PNG compression. Encoding resvg's RGBA buffer straight to JPEG (quality 82) via jpeg-js drops each card from ~693 KB to ~95 KB, and dist/client/og/ from 27 MB to 4.0 MB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
Sitemap is a prerendered server route alongside feed.xml; JSON-LD emits a PodcastSeries on the home page and a PodcastEpisode on each episode, with `<` escaped to < so content can never close the script block. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
The episode <h1> was single-line ellipsised with only a title= tooltip as the fallback, which is unreachable by keyboard or touch. It now wraps. Every page also gains a skip link, off-screen until focused, so keyboard users can jump the five-item nav; <main> gets the matching id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
Section markers referenced an implementation plan that no longer exists; headers now name what each section checks. Drops the newest-episode transcript and bundled-JS checks (the per-episode loops cover both) and the per-path existence loop in prerender coverage (every path is already asserted by its own section). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
The router shell and the static document handlers build their own markup, so a regression could hit one path and not the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
The 250-line ceiling made the stylesheet un-formattable: running Prettier on it expands the hand-compacted single-line rules past the limit and fails the build check. Formatting consistently is worth more than the size ceiling, which measured lines rather than anything that matters. .prettierignore excludes build output, the vendored fonts, and tests/fixtures — those are golden files compared byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
Pure formatting, no behaviour change. Verified: the built episode pages are byte-identical before and after, and the only diff on router pages is the Date.now() TanStack bakes into its serialized match state, which differs between any two builds. The markdown changes are four stray leading spaces before transcript timestamps; tests, typecheck and the build check all pass unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk
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.
Two logical halves on one branch. Review them as such — commits 1–3 are the bug fixes, 4–7 are the metadata work.
Part 1 — eight correctness bugs (
24a31f9,ffacdd1,9e7d21d)Build, tests and
npm run checkwere green before and after all of these. Every one failed silently.24a31f9— ingest / publish-transcriptfetchDescriptTranscriptonly checkedres.ok. A 200 with an empty body flowed through toreplaceTranscript, which wrote the file with the whole committed transcript deleted — git was the only recovery. Now throws beforewriteFileSync.frontMatter.people = item.peopleoverwrote hand-curatedpeoplewith[]when a feed item had no<podcast:person>tags.Number("01:09:58")isNaN, so a legalHH:MM:SS<itunes:duration>silently dropped the whole feed item.ffacdd1— content parsing# Transcriptnever matched"\n# Transcript\n", so the entire transcript was reclassified as outline and the page rendered empty.datebecame<pubDate>Invalid Date</pubDate>in the feed, and threwRangeErrorpartway throughpublish-atproto's loop — after earlier files were mutated and records already PUT. Now fails at parse time, naming the episode.toSeconds("")returned0rather thanundefined.9e7d21d— "Home" and "Episodes" both linked to/. Episodes now points at/#archive.Part 2 — link previews and SEO (
2c0a6e8,d075e18,ce8f2db,217c1b7)The site previously had no
og:image, no sitemap, no robots.txt and no structured data, and episode<title>s said "This Month in React" twice.Per-episode Open Graph cards.
scripts/og.tsrenders a 1200×630 card per episode at build time viasatori+@resvg/resvg-js— show artwork on the left, episode title, month, hosts and runtime on the right. Runs inpostbuild, offline and deterministic. Inter (OFL, latin subset) is committed underassets/fonts/; the show artwork is committed aspublic/artwork.jpg.JPEG, not PNG. The artwork's film grain defeats PNG compression — cards were 677 KB each, 27 MB total. resvg's raw RGBA buffer now goes straight into
jpeg-jsat q82, skipping PNG encoding entirely: 93 KB each, 4.0 MB total, visually identical.Full metadata.
og:type/url/site_name/title/description/locale, absoluteog:imagewithwidth/height/type/alt,article:published_time, andtwitter:card=summary_large_image(this is what makes Discord render the card full-width instead of a thumbnail) with matchingtwitter:*tags.theme-coloris set to the card's navy so Discord's accent bar matches the image. Episode<title>no longer repeats the site name.Audio in the preview — best effort. Episode pages emit
og:audio,og:audio:secure_urlandog:audio:type. Both Discord and Slack gate inline players and neither reliably grants one to an arbitrary site, so treat this as "correct tags emitted", not "confirmed playable". Worth testing with a real link before relying on it.twitter:playerwas deliberately skipped — it requires Twitter allowlisting.Discovery and structured data.
public/robots.txt, a prerendered/sitemap.xml(42 URLs,/searchexcluded,<lastmod>per episode),PodcastSeriesJSON-LD on the home page andPodcastEpisodeon episode pages, withassociatedMediaas anAudioObject. JSON-LD escapes every<as<so nothing in a title or description can close the<script>block — there's a test for it.Verification
npm test72 pass / 0 fail ·tsc --noEmitclean ·npm run buildsucceeds ·npm run check514 assertions, 0 failures. Regression tests added per bug, each confirmed failing against the pre-fix source. Generated cards were inspected visually, not just asserted to exist.Also cleaned up
<h1>no longer single-line-ellipsised (thetitle=tooltip fallback was unreachable by keyboard and touch); it wraps.id="main"on<main>— asserted on both rendering paths, since the router shell and the static document handlers build their own markup.check-build.mjs: plan scaffolding comments gone, duplicate assertions removed (514 → 428 checks, all 86 accounted for as duplicates), sections renamed to describe what they check. Failure path re-verified — the script still exits non-zero on a missing output.Still open
24% of outline anchors resolve to no section;
@vitejs/plugin-rscis enabled but unused; a handful of tests can't fail. The repo is not prettier-clean onmain(20 files) — and notepublic/styles.cssis deliberately hand-compacted with single-line rules, so running the formatter on it doubles its length and trips the "under 250 lines" build check. It should probably be added to a.prettierignore.🤖 Generated with Claude Code
https://claude.ai/code/session_01BcUupktNPKyqJcPwCVEhLk