Skip to content

feat(search): stream /search from the app router with SSR seeding - #947

Draft
thostetler wants to merge 10 commits into
adsabs:masterfrom
thostetler:feature/ssr-search-results
Draft

thostetler wants to merge 10 commits into
adsabs:masterfrom
thostetler:feature/ssr-search-results

Conversation

@thostetler

@thostetler thostetler commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

/search now renders on the server and streams, cutting median time to readable results from 13976ms to 1892ms (n=13/arm) and LCP from 12599ms to 1346ms. TBT is unchanged at ~3.1s: the page is readable sooner, not interactive sooner.

  • other routes stay on Pages Router, so /search navigation is a full page load
  • every /search request hits Solr now, including bots; the old route didn't
  • anonymous site-alert dismissal doesn't cross the Pages/App Router boundary
  • drops react-shepherd for direct shepherd.js
  • result cards are fixed height: titles clamp to 2 lines, full title on hover

@thostetler
thostetler requested a review from shinyichen October 5, 2026 02:39
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Bundle size

Shared by all pages: 629.8 kB (+2.0 kB) 🔴

Route Base Head Change
/search 1089.7 kB — 🗑️ removed
/abs/[id]/credits 1062.4 kB 1043.4 kB -19.0 kB 🟢
/abs/[id]/coreads 1062.4 kB 1043.5 kB -19.0 kB 🟢
/abs/[id]/references 1062.4 kB 1043.4 kB -19.0 kB 🟢
/abs/[id]/toc 1062.4 kB 1043.4 kB -19.0 kB 🟢
/abs/[id]/mentions 1062.4 kB 1043.4 kB -19.0 kB 🟢
/abs/[id]/citations 1062.4 kB 1043.5 kB -19.0 kB 🟢
/abs/[id]/similar 1062.4 kB 1043.5 kB -19.0 kB 🟢
/search/citation_helper 1070.9 kB 1052.5 kB -18.5 kB 🟢
/user/libraries/[[...id]] 1068.1 kB 1049.8 kB -18.3 kB 🟢
/public-libraries/[[...id]] 1067.9 kB 1049.6 kB -18.3 kB 🟢
/abs/[id]/abstract 1113.1 kB 1095.7 kB -17.4 kB 🟢
/abs/[id]/graphics 746.8 kB 732.0 kB -14.8 kB 🟢
/user/account/register 645.9 kB 647.9 kB +2.0 kB 🔴
/user/notifications 689.6 kB 691.6 kB +2.0 kB 🔴
/user/account/login 635.0 kB 637.0 kB +2.0 kB 🔴
/user/account/verify/reset-password/[[...verifyToken]] 644.7 kB 646.7 kB +2.0 kB 🔴
/not-implemented 628.3 kB 630.3 kB +2.0 kB 🔴
/historical 645.5 kB 647.5 kB +2.0 kB 🔴
/user/orcid/OAuth 630.6 kB 632.6 kB +2.0 kB 🔴
/journalsdb/[[...bibstem]] 637.4 kB 639.4 kB +2.0 kB 🔴
/user/account/forgotpassword 642.7 kB 644.7 kB +2.0 kB 🔴
/browse 817.3 kB 815.3 kB -2.0 kB 🟢
/user/orcid 642.5 kB 644.5 kB +1.9 kB 🔴
/paper-form 683.2 kB 685.1 kB +1.9 kB 🔴
/_error 630.3 kB 632.2 kB +1.9 kB 🔴
/classic-form 705.2 kB 706.6 kB +1.4 kB 🔴
/search/authoraffiliations 675.1 kB 676.6 kB +1.4 kB 🔴
/search/exportcitation/[format] 738.8 kB 740.2 kB +1.4 kB 🔴
/ 692.5 kB 693.7 kB +1.2 kB 🔴

…and 15 more routes changed.

First load = polyfills + shared _app chunks + route chunks, gzipped.

@thostetler
thostetler force-pushed the feature/ssr-search-results branch from 426142c to 4c4300c Compare October 5, 2026 02:54
The color-mode anti-flash CSS needs the palette but not the rest of
Chakra's theme, so theme-tokens.ts now holds the colors and config and
theme.ts composes from it. The landing and abstract tours import
shepherd.js dynamically instead of mounting react-shepherd's provider on
every route. bundle-sizes.mjs reads Next's route-bundle-stats.json so
app router routes appear in the report alongside pages router ones.
Query building, boost-type application, pagination maths, app-mode
resolution, and the prefs cookie move out of hooks and into plain
functions so an app router route can call the same code the pages router
one does. searchIdentity.ts hashes a query the same way for the
server-rendered seed and the client query, so React Query treats them as
one cache entry instead of refetching on mount.
next/router and next/navigation expose different shapes for the same
concepts, so components need one contract that works under either while
the two routers coexist. Components that read the router directly now
go through the adapter. Its pathname is always the resolved path, not
the route pattern, because callers build URLs from it.
Adds the root layout, splits providers so both routers share them, and
gives server components one session resolver and a server-rendered
site-wide alert. Middleware forwards the access token on a request
header so a request whose session cookie is not yet written can still
resolve a token; the resolver prefers the cookie and falls back to the
header. The token travels on request headers only and never reaches
the browser.
Moves the search page out of pages/search/index.tsx into
components/SearchPage and adds an app router /search route that seeds
the primary result list from the server, streaming the facets and the
rest of the chrome in behind Suspense with skeletons sized to the
loaded page. Median time to first result drops from 14.0s to 1.9s and
LCP from 12.6s to 1.3s on a throttled connection. Total blocking time
is unchanged at roughly 3.1s, so the page becomes readable sooner but
not interactive sooner.
@thostetler
thostetler force-pushed the feature/ssr-search-results branch from 4c4300c to cd73896 Compare October 5, 2026 14:50
@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.43813% with 111 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.5%. Comparing base (273327f) to head (2d06ca0).

Files with missing lines Patch % Lines
src/components/NavBar/NavMenus.tsx 0.0% 24 Missing ⚠️
src/components/SearchPage/SearchPage.tsx 61.2% 12 Missing and 2 partials ⚠️
src/components/ResultList/ListActions.tsx 42.9% 9 Missing and 3 partials ⚠️
src/lib/useRouterCompat.tsx 85.9% 10 Missing and 1 partial ⚠️
src/components/SiteAlert/SiteAlert.tsx 79.5% 4 Missing and 3 partials ⚠️
src/components/NavBar/useTour.ts 0.0% 6 Missing ⚠️
src/components/NavBar/AppModeUrlNotice.tsx 66.7% 2 Missing and 3 partials ⚠️
src/lib/pagination.ts 91.4% 4 Missing ⚠️
src/components/Notification/Notification.tsx 86.4% 1 Missing and 2 partials ⚠️
src/lib/serverside/siteWideMsg.ts 93.5% 2 Missing and 1 partial ⚠️
... and 14 more
Additional details and impacted files
@@           Coverage Diff            @@
##           master    #947     +/-   ##
========================================
+ Coverage    55.1%   56.5%   +1.5%     
========================================
  Files         374     400     +26     
  Lines       11462   12117    +655     
  Branches     2514    2650    +136     
========================================
+ Hits         6307    6846    +539     
- Misses       4524    4594     +70     
- Partials      631     677     +46     
Files with missing lines Coverage Δ
src/api/search/search.ts 81.5% <100.0%> (-0.3%) ⬇️
src/api/search/searchQueryIdentity.ts 100.0% <100.0%> (ø)
src/app/global-error.tsx 100.0% <100.0%> (ø)
src/color-mode-no-flash.ts 100.0% <ø> (ø)
src/components/AllAuthorsModal/AllAuthorsModal.tsx 24.2% <100.0%> (+17.6%) ⬆️
src/components/NavBar/FeedbackDropdown.tsx 86.4% <100.0%> (ø)
src/components/NumFound/NumFound.tsx 52.2% <ø> (ø)
src/components/NumFound/numFoundHeight.ts 100.0% <100.0%> (ø)
src/components/ResultList/Item/Item.tsx 51.8% <100.0%> (+19.5%) ⬆️
...mponents/ResultList/Item/ItemResourceDropdowns.tsx 55.5% <100.0%> (+2.8%) ⬆️
... and 56 more

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Result cards, the actions toolbar, and the year histogram now reserve the same height loading and loaded, with skeletons matching the real layout. ADS_COMPAT no longer AND-joins the database clause into fq_database on every pass.
useColorModeColors threw during the server render of the Suspense fallback, which dropped the whole seeded result list back to a client fetch. Uses _dark style props instead.
…inert

Clamping the author row hid the all-authors trigger on narrow screens, so only the names clamp now. The loading toolbar is inert rather than merely dimmed, and the result-card chrome in the items skeleton is opt-in so the abs and library lists keep a neutral placeholder.
Load the FontAwesome stylesheet at build time instead of letting the core package inject it from JS, so the toolbar icons are sized on first paint rather than after hydration. Render the result meta separators from CSS rather than useBreakpointValue, which resolved to undefined on the server and bunched the row until it hydrated. Group the skeleton header the way SearchPage groups it so the stack emits one gap and not two. Citation links pick up the hover underline the title already had.
The verify-success test waited for the URL with notify still attached, but the app strips that param once the toast consumes it, racing hydration. Assert the redirect target from response.url(), check the toast text, then wait for the settled URL.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant