Repository navigation
Dev - #37
Merged
Merged
Dev#37
Conversation
The sync Lambda only upserted, so retracted and renamed planets accumulated in the table forever -- 13 stale records over ten months, inflating every count the UI derives from /api/planets. Adds a sweep pass that runs after a successful ingest, diffing the archive's pl_name set against a projection-only paginated scan and deleting the difference. It refuses to delete more than 5% of the table in one run, so a truncated fetch aborts loudly rather than emptying the table, and it never raises -- the upserts have already committed and must still report success. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…14) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
table.batch_writer() flushes every 25 items, so a throttle partway through can commit several batches before raising. The failure path returned deleted=() regardless, publishing total_removed: 0 for a run that really did delete records — the same silent-no-op signal this PR exists to remove. Rename SweepResult.deleted to submitted and accumulate names as they are handed to the writer, so the count bounds what may have committed instead of understating it. Names are recorded before delete_item because a buffered item can commit in the very flush that raises. The fake table hid this: batch_writer() returned self and delete_item wrote through immediately, so committed and reported were equal by construction. Replace it with a fake that mirrors boto3's BatchWriter — buffer, flush at 25, flush the remainder on exit — tracks handed and committed names separately, and can model a batch that commits and still raises, since BatchWriteItem is not atomic. Also scope the INFO level to the sweep logger rather than the root logger, and stop the README claiming the table cannot drift above the archive's count when a tripped guard deliberately leaves drift.
…mantics Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Table view is the default, so a keyboard or screen-reader user could not open a planet at all: rows opened on <tr onClick> and sort headers on <th onClick>, neither reachable. Rows now carry a real button on the planet name rather than a focusable <tr>: role="button" on a row strips the row/cell semantics screen readers use to navigate a table, and a named button also announces which planet it opens. The row keeps its pointer affordance and no longer double-reports. Sort headers collapse into a SortableHeader that renders the button and aria-sort together, so direction is no longer carried only by a glyph, and the unsortable Star column stops claiming to be clickable. ESIModal already met the dialog standard while PlanetModal had none of it. Rather than copy the focus trap, extract it into ModalDialog, which owns the overlay, portal, escape, scroll lock, focus trap and focus restore, and cannot be rendered without role/aria-modal/aria-labelledby. Its focus trap also recognises form controls, which the ESIModal original did not. Also: decorative glyphs are aria-hidden, the nav hamburger reports aria-expanded/aria-controls, view toggles report aria-pressed, and the nav links use next/link instead of forcing a full reload. Closes #6 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d-records fix: sweep records the NASA archive no longer lists
…estions closed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pense answer - Band edges moved to actual Pecaut & Mamajek 2013 values (33000/10000/7300/ 6000/5300/3900/2300); Harvard set noted as rejected alternative (K/M edge mislabels ~146 M-dwarf hosts on the flagship red-dwarf filter) - ?sort=key.dir added to URL schema, Goals, AC, task 1; out-of-scope alternative noted and rejected - Suspense punt replaced with the build-verified answer (no boundary needed on Next 16.3.3; /explore stays static) - Period span corrected to ~8 orders; slider bounds derived from data - Results total computed from fetched list, never a literal - Memoisation AC added; planetFilters.ts ~200-line split threshold noted Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed to table as props Addresses round-2 review on PR #39: single URL writer (no second debounced router.replace racing on the query string), Back/Forward can't desync the sort indicators. Also: view toggle explicitly out of scope, K/M mislabel count corrected to ~150 (157 as of review re-run). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
spec: explore filters
The trap returned early when the dialog had no focusable stops, so neither wrap branch could match and native Tab walked out of a dialog claiming aria-modal="true". Swallow the key instead. No current caller hits this — both dialogs carry a Close button — but the primitive exists so the next author does not have to remember. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…= and ?sort= (#43) * feat(explore): add planetFilters, the one owner of filter semantics Holds the FilterState type, the URL codec and the predicate engine, so the query-string schema exists in exactly one place. The default sort is omitted from the URL and anything malformed parses to an inactive filter rather than an error state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(explore): bind filter state to the URL with useFilterParams The URL is the source of truth; local state holds only the edit that has not been written yet, and the URL reclaims it whenever it moves on its own. Writes are debounced and use replace, so Back does not step through keystrokes, and a queued write is dropped once the URL it was queued from has changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(explore): filter both views from one source of truth Search, discovery method and sort leave PlanetTable's local state for a single useFilterParams in ExploreClient, which applies the filter once and hands the table a sorted-in-place-free list plus sort props. The grid was never filtered before; it is now, from the same set. Pagination gains two independent guards: an explicit reset to page 1 when the visitor changes a filter, and a clamp to the last real page that also covers Back/Forward, where no reset runs. "Page 1 of 0" is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): keep typing that lands mid-write, clamp the page in state Two bugs the review caught, both reproduced as failing tests first. useFilterParams could not tell its own URL write from an external one, so a keystroke arriving while its write was in flight was discarded along with the write it had queued — the input reverted under the visitor mid-typing. The hook now remembers the queries it dispatched and only surrenders the edit to a query it never asked for. Debouncing moved into an effect so its cleanup cancels the superseded write instead of a manual clearTimeout keyed on the URL. The page clamp was derived and never written back, so every reader had to remember to use the clamped copy and the grid's Previous button did not — it dead-clicked whenever the clamp was active. usePagination now hides the raw request entirely, which makes that mistake unrepresentable rather than fixed. Also: an unknown ?method= renders as its own option instead of the select falsely reading "All Types" over zero planets, and serializeFilters writes the trimmed search it actually applies. The router mock in both test files returned a fresh object per render, which re-ran effects that depend on it and hid missing dependencies; it is stable now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(explore): give usePagination sole ownership of the paging rules PlanetTable took page + itemsPerPage + onPageChange and recomputed the page count itself, duplicating the rule usePagination was added to own. It now takes the hook's return value as one Pagination prop. pageItems joins the contract because totalPages alone is not enough: a table that reads the page count from the hook but slices with its own itemsPerPage can show "Page 1 of 3" over every row it was handed. The offset arithmetic was duplicated in both pagination UIs anyway, so the hook absorbs it and neither caller keeps an unclamped page or a second copy of itemsPerPage in scope. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(explore): pin that both views page through one page size Grid and table now share a page size only because they share the hook. Nothing failed if the grid grew its own again, so this asserts the page label and the item count survive a view switch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(explore): add radius, mass and orbital period range filters Ranges live in the URL as `min..max` with either end omissible, in real units, alongside the search/method/sort params task 1 established. The codec and predicate stay in planetFilters.ts; a planet with no measurement for a quantity drops out only while that range is active, and the control says so in plain language. Paired number inputs are the primary mechanism, with a log-scale dual-thumb track over two native range inputs as the enhancement. Track bounds come from the fetched data's own min/max, so the ~8 decades of orbital period stay reachable instead of collapsing against the tail. Also adds pl_orbper and st_teff to the API projection; st_teff is task 4's input, added now to avoid a second one-line change to the same list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): announce thumbs in real units and stop them collapsing Review follow-ups on the range filters. Sliders announced a log-track coordinate (a 10 Earth-radii bound read as "333"). Both thumbs now carry aria-valuetext in the measured unit, matching what the neighbouring number input shows. The upper thumb could end up with min === max, which Chromium paints at the LEFT end of the track whatever its value: "radius at least 176" drew thumbs at both ends, reading as the whole range selected when it is the narrowest filter. Both inputs now span the full track and crossing is prevented when the bound is committed, so no input is ever degenerate. With the thumbs no longer constrained by each other they can stack at the top of the track, where only the lower one can move, so it is raised to take the pointer. A crossed pair from the URL or the number inputs drew the range it swaps into while matching nothing; the track is now dropped instead. st_teff left the /api/planets projection. Nothing reads it yet, so it was a per-planet payload cost for every visitor, and its test named a filter that does not exist. It returns with the star type filter that uses it. * fix(explore): keep the range rail mounted while the bounds cross Only the thumbs come off now. Unmounting the whole track container made the filter card lose 32px on the first keystroke of most typed maximums, jerking itself and the results below it, and putting it back on the next keystroke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnax-builder[bot] <aronnax-builder[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com>
…48) * feat(explore): multi-select discovery method and one shared ordering Widen ?method= to a comma-separated contract and hoist the sort comparator out of PlanetTable so both views order before paginating. Method becomes a list in FilterState, so "no filter" is the empty array rather than a null/empty-string pair. The predicate takes the union, so naming a method the data lacks alongside a real one no longer collapses the result set to nothing. withMethod sorts the selection, giving one set of methods exactly one URL however it was clicked. The comparator moves to lib/planetFilters.ts as sortPlanets, called once in ExploreClient. Grid and table now render the same ordered array, so grid page N and table page N hold the same planets for a shared URL -- they did not before, which defeated the point of putting sort in the URL. The single-select dropdown becomes a checkbox fieldset rather than a <select multiple>, which is hostile on touch and to screen readers. * fix(explore): stop a method checkbox unmounting under the visitor mid-click The offered boxes were derived from filters.methods, which those same boxes mutate. Unticking a method no planet carries — one only the URL named — dropped it from the selection, so its own <label> unmounted mid-interaction: keyboard focus fell to <body>, and the box was gone for good with no way to re-tick it. The offered set now only grows within a mount, adjusted during render like useFilterParams does rather than in an effect, so no pass ever paints without the box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): canonicalise ?method= order on parse as well as serialize withMethod sorted the selection but parseMethods kept URL order, so untick and re-tick from ?method=Transit,Astrometry produced a second URL for the same set. Sorting on the way in too makes "one set of methods, one URL" actually hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnax-builder[bot] <aronnax-builder[bot]@users.noreply.github.com> Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ard-accessibility fix(explore): make the table, rows and modals keyboard accessible
…pty state (#50) * feat(explore): star type filter, clear all, live results count and empty state Adds st_teff to PLANET_SUMMARY_FIELDS so the projection carries the host temperature the new filter bands, then derives O/B/A/F/G/K/M from it. The band table lives in lib/starBands.ts rather than planetFilters.ts, which the spec calls for once that file passes ~200 lines. Bands are half-open with the lower bound inclusive, on the Pecaut & Mamajek edges the spec pinned. A host cooler than 2,300 K or with no measured temperature is unclassified, not M, and drops out only while the star filter is active. Canonical ordering of a multi-select moves into serializeFilters, so it now lives in exactly one place instead of being repeated across the parser and each mutator. Methods order alphabetically and star classes by temperature; either way one set of selections has exactly one URL. Clear all resets to DEFAULT_FILTERS and so strips the query string. Both it and the empty state's button return focus to the search box, since each stops being focusable the moment it works. The results count spans both views, which the grid's inline markup could not, so the grid becomes PlanetGrid owning its own pagination like PlanetTable does. That left two copies of the pagination footer, now one PaginationControls; the "(N planets)" it used to carry is what the live count states, better, for both views. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(explore): keep sort through Clear all, own the failed-load state, correct the O-type label Addresses the three blockers on PR #50. Clear all no longer discards the sort. `cleared(state)` now lives beside `isFiltered` in planetFilters.ts, so the one module that decides what counts as a filter also decides what clearing throws away; the two can no longer disagree about sort the way DEFAULT_FILTERS did. The explore page distinguishes a failed load from an empty archive. It fetched into `planets: []` on any failure, so a dropped connection rendered NoResults telling the visitor the archive was empty — the opposite of what happened. The page now holds a loading/unavailable/loaded union, shows its own error panel with a working retry, and never hands an empty list to the explorer on failure. That makes NoResults' empty-archive branch true rather than merely vague. O is labelled "O — blue": the table bands on temperature alone, off the Pecaut & Mamajek dwarf calibration, so it cannot name a luminosity class. Also closes both non-blocking notes. PaginationControls moves focus to the page indicator when Previous/Next disables itself, matching the clear buttons and leaving focus on the button while it stays usable. The results count moves into ResultsCount, which shows the number instantly but announces it only once the count settles, so a screen reader is not read a fresh total per keystroke. --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ters Merge rather than rebase, per the epic's ground rules — the epic branch is shared and has had PRs against it. Two conflicts, both where #6 and the filters epic changed the same code: - PlanetTable.tsx — #6's SortableHeader wins over the epic's inline `<th onClick>` headers, but keeps the epic's hoisted props (pagination, sortKey/sortOrder/onSort). The table stays a controlled component; the header just becomes a real button carrying aria-sort. Drops the now redundant esiAriaSort and the useState/useMemo #6 still needed. - ExploreClient.test.tsx — add/add. Kept the epic's file and folded #6's two view-toggle tests into it using its existing helpers; a separate file would have duplicated the whole next/navigation mock harness. The epic's two `it.fails` in PlanetTable.test.tsx were the accessibility gaps #6 fixes, so they become real passing tests. The keyboard-activation one is rewritten for the controlled table: it now asserts every sortable column reports its key through onSort, and that aria-sort follows the props. Chaining clicks through one header after another cannot work now that the table no longer owns the sort state. Verified: 422 pass, tsc 0, eslint 0, next build clean with /explore still Static, no orphaned CSS in PlanetTable.module.css.
merge: bring dev (keyboard accessibility #6) into epic/41-explore-filters
) * docs(roadmap): record archive-first direction and the filters build Item 3 described "improve advanced filters" for filters that did not exist. Reword it to what the epic built: search, radius/mass/period ranges, discovery method, star type, and all filter and sort state carried in the URL. Add a Direction section recording the lane settled in #36 — archive first, with anything presented as new derived from the sync diff rather than hand-written. "Current Focus" becomes "Shipped": all three items under it are complete, so marking only item 3 would have implied the other two were still pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(roadmap): separate shipped diff surfaces from intended ones The Direction paragraph named recent confirmations, retractions and records broken as things the site already derives from the sync diff. Only the latest-confirmations feed exists; retractions cannot exist yet because the sweep hard-deletes removed planets (#40). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
feat: explore filters — search, ranges, method, star type, all in the URL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The file had reached 871 lines across ten describe blocks sharing a ~120-line preamble. Extract that preamble into exploreTestUtils.tsx and split the suites by concern: filters, URL state, and view. Tests directly assigned the module-level `query` binding to set the URL before rendering. That binding cannot cross a module boundary, so the helper exposes startAtUrl() in its place; every other line of every test moved unchanged. Closes #56 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cost, next/link, st_logg (#51) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k-look button in name <td> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lib/planetUrl.ts owns the whole /planet/<name> mapping in both directions so no other file encodes or decodes a planet URL. Names round-trip exactly per spec decision 1 — spaces, plus signs, parentheses and dots survive; an unusable segment decodes to null so the route can 404 instead of throwing. lib/planetDetail.ts is a server-only GetItem by pl_name returning the full stored item or null, wrapped in React cache() so generateMetadata and the page share one read. Read failures propagate rather than resolving null, keeping an outage out of the ISR-cached 404 path. Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ound (#63) * Add /planet/[name] route with grouped stat sections Server-renders a planet profile from the archive item: Planet, Star, System and Discovery sections as label/value pairs under one h1, plus a provenance line citing the archive and the record's sync date. planetStatSections owns field selection, units and null handling so the route renders sections without knowing what a field means. Every unknown degrades to an em dash with screen-reader copy rather than a dropped row. A malformed segment and an unstocked name resolve to the same notFound(), whose boundary recovers the attempted name from the URL to prefill the explore search. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Add planet page error boundary and cap the reflected URL name An outage propagates by design from #61, but this route was its first consumer and the throw landed on Next's default "Application error" screen — worse degradation than the home page, which says "Latest discoveries are unavailable right now". error.tsx follows the explore Unavailable precedent: role="alert", branded copy, reset() retry and an /explore way out. The not-found page reflected the URL segment as displayed text, so an arbitrary sentence could be styled onto our domain. planetNameFromParam now rejects a segment longer than any archive designation, which fixes both consumers at once: the lookup 404s without a read, and the not-found page falls back to "Browse the archive". Also folds in the reviewer's starBandOf note, so planetStats no longer re-looks-up a class to recover its label, and renames a page test that claimed to observe a cache() dedupe it cannot see through the mock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: exoplanet-builder[bot] <exoplanet-builder[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com>
* Add the ESI badge and Earth comparison to the planet page The page carried the archive's numbers but nothing that told a non-astronomer what they meant. earthComparisons() turns the five Earth-relative fields into sentences: proportions read as percentages below Earth and multiples above it, kelvin becomes °C and °F against Earth's own airless figure, and an orbit is counted in the unit that keeps it legible — hours, Earth days or Earth years. The badge gains a page variant rather than a fork. Placement and scale were the only card-specific rules in its stylesheet, so they move out of .badge into .card, leaving .badge as the shared pill and .page as the larger standalone presentation. PlanetCard is untouched. The Kepler-452 b fixture stored esi 0.83; the sync writes round(100 * geometric_mean), so it is 83. * Keep Earth-comparison numbers consistent with the Planet section Two significant digits rounded whole-number counts to values the archive never held: a 267-day orbit printed as 270 beside "267.3 days" in the Planet section, 365 days printed as 370 (longer than the Earth year the branch had just ruled out), and 3,247x starlight as 3,200x. Format whole above 10 and keep 2 significant digits below, where they buy legibility. Also name compute_esi in the 255 K comment so the baseline is not later aligned with the sync, and give the badge the focus ring its sibling controls define, now that it is the page header's only control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: exoplanet-builder[bot] <exoplanet-builder[bot]@users.noreply.github.com> Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ags (#67) * Add planet-specific metadata to /planet/[name] Shared links previewed as a bare domain because the route inherited the site-wide title and description. generateMetadata now returns a title, stat-bearing description, and OG/Twitter tags per planet, reusing the cache()d getPlanetDetail through the existing loadPlanet seam so titling and rendering still cost one GetItem. planetHighlights lives in planetStats beside the stat table it must agree with, so a preview cannot quote a figure the page contradicts. A planet the archive only names degrades to a plain sentence rather than dangling commas, and an unknown name gets noindex 404 metadata instead of throwing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Point metadataBase at the serving host, via one shared origin The apex 307s to www, so every canonical and og:url this epic emits resolved to a redirecting host — the exact www/apex ambiguity a canonical exists to settle, and a temporary redirect is the weakest signal for it. Task 5's sitemap would have inherited the same URLs, which Search Console reports as "Page with redirect" and declines to index. SITE_ORIGIN lives in lib/site.ts so the sitemap and OG image routes import the origin instead of retyping the literal, and site.test.ts pins both the host and the no-trailing-slash contract those callers concatenate against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…lanet/[name] (#69) * Link planet names to /planet/[name] across card, table, modal and discoveries Every internal link is built with planetUrl(), so no component encodes a planet name itself. PlanetCard and LatestDiscoveries: the <h3> name becomes a next/link. Their "Learn More" / plain-text behaviour is otherwise unchanged. PlanetModal: adds a "View full profile" link as the next step out of the quick look. The dialog had one focus stop before this, so the trap now cycles two. PlanetTable (spec decision 5): the name button becomes a next/link that stops propagation, and each row gains a quick-look button labelled "Quick view: <name>" inside the existing name cell. Keyboard users keep a modal entry point and gain a navigation one; mouse row-click still opens the modal. Also lifts a shared PageSection that owns the aria-labelledby wiring, and moves both planet-page section components onto it. Closes #68 * Lift PlanetNameLink; make the quick-look icon presentation-explicit Review on #69 flagged the planet-name link hand-rolled at three call sites with two byte-identical CSS blocks and a third near-copy. components/planet/PlanetNameLink owns planetUrl and the treatment; card, discoveries and table call it. planetUrl now has one caller in the component tree instead of three. The modal's "View full profile" stays out of it: its label is a CTA, not the planet name. Settles the underline divergence on always-underlined, matching the planet page's .provenanceLink. Hover-only underline gave touch users no affordance at all, since there is no hover to reveal it. The module carries no 'use client', so it still server-renders inside LatestDiscoveries and takes the stopPropagation the clickable table row needs from PlanetTable. U+1F441 is Emoji_Presentation=No, so a bare glyph asked for a text presentation most system fonts lack. Replaced with an inline svg that inherits currentColor and sizes to the 28px circle, plus title="Quick view" so a sighted mouse user can tell it from watch/favourite. Tightened the row-name assertion: it was a substring match that passed on "Alpha b<glyph>" and could not fail as named. --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com>
* Planet pages 5/7: sitemap covering every archived planet URL Adds app/sitemap.ts so all ~6k /planet/<name> URLs are indexable directly rather than only by crawling the explore table. Extracts the paginated LastEvaluatedKey walk out of app/api/planets/route.ts into scanAllPlanets() in lib/dynamo.ts, parameterised by projected field and typed as Pick<Planet, Field>[] so both call sites keep exact row types. The route's revalidate export would have prerendered the sitemap during next build, which fails wherever Dynamo is unreachable — CI has no AWS credentials. connection() defers the Scan to request time and the hour moves onto the data cache, which also bounds a public endpoint that would otherwise Scan the table once per crawler request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Advertise the sitemap in robots.txt and drop the false lastmod Nothing pointed crawlers at /sitemap.xml, so the ~6k planet URLs it lists were no more discoverable than before. app/robots.ts allows crawling and names the sitemap at its absolute canonical URL; it is static, so it adds no build-time Dynamo dependency. last_updated is a sync-run stamp, not a modification time: the sync computes one timestamp per invocation and writes it to every row on a 6-hour schedule, so every <lastmod> was identical and advanced four times a day whether or not a planet changed. Google distrusts lastmod site-wide when it is inaccurate, so omit it — which also drops parseTimestamp and narrows the Scan projection to pl_name alone. scanAllPlanets now takes a non-empty tuple, making the empty field list that would build an invalid ProjectionExpression unrepresentable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add per-planet OG image so shared planet links unfurl as a card Every /planet/<name> page advertised summary_large_image with no image, so a shared link rendered as a blank panel. app/planet/[name]/opengraph-image.tsx now generates a 1200x630 PNG from the planet's own record. shareCard() holds what the card says and how it is coloured, so the route file is layout only and the copy is testable without rendering a PNG. Distance and discovery-year phrasing moves into planetStats alongside planetHighlights, which now shares the light-year phrase rather than rebuilding it. ESI bands name their theme colour slot instead of deriving it from array position, because satori cannot resolve a CSS custom property and needs the hex. SITE_THEME gives the layout and the card one source for the theme the site renders, so the image cannot silently drift from the page. A name the archive does not stock renders a branded fallback rather than a 500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Render the OG card through real satori and drop weights it cannot apply next/og ships a single Geist face and the route passes no `fonts`, so the 600/700 declarations on the wordmark, hero and badge selected nothing. The card's hierarchy is size, colour and shape; the declarations only misled. The mocked suite asserts copy but never reaches satori, which rejects a multi-child <div> with no explicit display — a 500 on the one route whose job is to never break an unfurl, invisible to tsc, eslint and every test. A sibling file mocking only planetDetail renders the planet and fallback paths for real and checks the PNG header, in ~180ms with no network. Also from review: pin each ESI band's tier to its position so colour and range cannot drift apart, default getTheme to SITE_THEME, tie SITE_THEME to theme.json's defaultTheme, resolve the palette once in shareCard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The quick look, card and table each rounded planet fields by hand, so the
same planet read "551.71 parsecs" in the modal and "551.7 parsecs" on the
page it links to. Each also used a falsy check, reporting a measured zero
as unknown and leaving the unit stranded beside it ("N/A parsecs").
planetKeyStats() gives the modal its six stats through the same formatters
the page sections use, and measurement() is now shared with the card and
table so a unit abbreviation is the only thing they choose for themselves.
Co-authored-by: aronnaxp <aronnaxp@users.noreply.github.com>
Stale lockfile resolutions were pinning vulnerable transitives that already had patched releases satisfying their parents' ranges. Audit goes 28 -> 0, and 3 -> 0 in the production tree. package.json is unchanged: `pnpm update` rewrites the caret floors, so the manifest was restored and `pnpm install` re-run, leaving the fresh resolutions bound to the original declared ranges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Names the planet in its Open Graph image alt so the share card is described accurately to screen readers and link previews, and adds the epic #59 entry to ROADMAP.md. The route-level alt export stays constant because adding generateImageMetadata would move the route to /opengraph-image/[__metadata_id__]; per-planet alt is supplied from page metadata instead. Closes #74
…resh chore(deps): refresh pnpm lockfile to clear all advisories
feat: shareable planet pages — /planet/[name]
|
@zackrylangford is attempting to deploy a commit to the CloudZack Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
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.
No description provided.