@@ -72,19 +72,23 @@ function ErrorPage({
7272 possibleMatchesQuery ?: string
7373 heroProps : HeroSectionProps
7474} ) {
75- const resolvedHeroProps : HeroSectionProps = possibleMatches ?. length
76- ? {
77- ...heroProps ,
78- arrowUrl : '#possible-matches' ,
79- arrowLabel : 'Possible matches' ,
80- }
81- : articles ?. length
75+ // Only inject the "arrow down" helper link when the caller didn't provide an
76+ // explicit action. Otherwise, it can create duplicate CTAs (notably on 404s).
77+ const resolvedHeroProps : HeroSectionProps = heroProps . action
78+ ? heroProps
79+ : possibleMatches ?. length
8280 ? {
8381 ...heroProps ,
84- arrowUrl : '#articles ' ,
85- arrowLabel : 'But wait, there is more! ' ,
82+ arrowUrl : '#possible-matches ' ,
83+ arrowLabel : 'Possible matches ' ,
8684 }
87- : heroProps
85+ : articles ?. length
86+ ? {
87+ ...heroProps ,
88+ arrowUrl : '#articles' ,
89+ arrowLabel : 'But wait, there is more!' ,
90+ }
91+ : heroProps
8892 return (
8993 < >
9094 < noscript >
@@ -108,7 +112,7 @@ function ErrorPage({
108112 ) : null }
109113 < HeroSection { ...resolvedHeroProps } />
110114
111- { possibleMatches ?. length ? (
115+ { possibleMatches ? (
112116 < PossibleMatchesSection
113117 matches = { possibleMatches }
114118 query = { possibleMatchesQuery }
@@ -140,15 +144,16 @@ function PossibleMatchesSection({
140144 const q = typeof query === 'string' ? query . trim ( ) : ''
141145 const searchUrl = q ? `/search?q=${ encodeURIComponent ( q ) } ` : '/search'
142146 const sorted = sortNotFoundMatches ( matches )
147+ const hasMatches = sorted . length > 0
143148
144149 return (
145150 < >
146151 < div id = "possible-matches" />
147152 < HeaderSection
148153 title = "Possible matches"
149- subTitle = { q ? `Semantic search for " ${ q } "` : 'Semantic search results.' }
150- cta = "Search the site"
151- ctaUrl = { searchUrl }
154+ subTitle = {
155+ q ? `Closest matches for " ${ q } "` : 'Closest matches.'
156+ }
152157 />
153158 < Spacer size = "2xs" />
154159 < Grid >
@@ -157,19 +162,19 @@ function PossibleMatchesSection({
157162 { sorted . slice ( 0 , 8 ) . map ( ( m ) => (
158163 < li
159164 key = { `${ m . type } :${ m . url } ` }
160- className = "rounded-lg bg-gray-100 p-6 dark:bg-gray-800"
165+ className = "rounded-lg bg-gray-100 p-4 sm:p- 6 dark:bg-gray-800"
161166 >
162- < div className = "flex items-start gap-4" >
167+ < div className = "flex items-start gap-3 sm:gap- 4" >
163168 < div className = "shrink-0" >
164169 { m . imageUrl ? (
165170 < img
166171 src = { m . imageUrl }
167172 alt = { m . imageAlt ?? '' }
168- className = "h-16 w-16 rounded-lg object-cover"
173+ className = "h-12 w-12 rounded-lg object-cover sm:h-16 sm:w-16 "
169174 loading = "lazy"
170175 />
171176 ) : (
172- < div className = "h-16 w-16 rounded-lg bg-gray-200 dark:bg-gray-700" />
177+ < div className = "h-12 w-12 rounded-lg bg-gray-200 sm:h-16 sm:w-16 dark:bg-gray-700" />
173178 ) }
174179 </ div >
175180 < div className = "min-w-0 flex-1" >
@@ -183,7 +188,7 @@ function PossibleMatchesSection({
183188 < span className = "truncate" > { m . url } </ span >
184189 </ div >
185190 { m . summary ? (
186- < p className = "mt-3 line-clamp-3 text-base text-slate-600 dark:text-slate-400" >
191+ < p className = "mt-2 line-clamp-3 text-base text-slate-600 sm:mt-3 dark:text-slate-400" >
187192 { m . summary }
188193 </ p >
189194 ) : null }
@@ -192,13 +197,13 @@ function PossibleMatchesSection({
192197 </ li >
193198 ) ) }
194199 </ ul >
195- { sorted . length > 8 ? (
196- < p className = "mt-4 text-sm text-slate-500" >
197- < a href = { searchUrl } className = "underlined" >
198- See all results
199- </ a >
200- </ p >
201- ) : null }
200+ < p className = "mt-4 text-sm text-slate-500" >
201+ { hasMatches ? 'None of these match? ' : 'No close matches found. ' }
202+ < a href = { searchUrl } className = "underlined" >
203+ Try semantic search
204+ </ a >
205+ .
206+ </ p >
202207 </ div >
203208 </ Grid >
204209 </ >
@@ -228,23 +233,38 @@ function FourOhFour({
228233
229234 const q = effectiveQuery ? effectiveQuery . trim ( ) : ''
230235 const searchUrl = q ? `/search?q=${ encodeURIComponent ( q ) } ` : '/search'
231- const heroActionTo =
236+ const hasPossibleMatches =
232237 Array . isArray ( possibleMatchesProp ) && possibleMatchesProp . length > 0
233- ? '#possible-matches'
234- : searchUrl
238+ const heroActionTo = hasPossibleMatches ? '#possible-matches' : searchUrl
239+ const heroActionLabel = hasPossibleMatches
240+ ? 'Possible matches'
241+ : 'Search the site'
242+
243+ // Most pages intentionally use the global `mx-10vw` gutter (it’s part of the
244+ // overall site layout). The 404 view reads better on mobile when it’s a bit
245+ // wider, so we override the underlying spacing token for just this subtree.
246+ const notFoundGutterStyle = {
247+ [ '--spacing-10vw' as any ] : 'clamp(0.75rem, 3vw, 3rem)' ,
248+ } as React . CSSProperties
235249
236250 return (
237- < ErrorPage
238- articles = { articles }
239- possibleMatches = { possibleMatchesProp }
240- possibleMatchesQuery = { effectiveQuery }
241- heroProps = { {
242- title : "404 - Oh no, you found a page that's missing stuff." ,
243- subtitle : `"${ pathname } " is not a page on kentcdodds.com. So sorry.` ,
244- image : < MissingSomething className = "rounded-lg" aspectRatio = "3:4" /> ,
245- action : < ArrowLink to = { heroActionTo } > Possible matches</ ArrowLink > ,
246- } }
247- />
251+ < div style = { notFoundGutterStyle } >
252+ < ErrorPage
253+ articles = { articles }
254+ possibleMatches = { possibleMatchesProp }
255+ possibleMatchesQuery = { effectiveQuery }
256+ heroProps = { {
257+ title : "404 - Oh no, you found a page that's missing stuff." ,
258+ subtitle : `"${ pathname } " is not a page on kentcdodds.com. So sorry.` ,
259+ image : < MissingSomething className = "rounded-lg" aspectRatio = "3:4" /> ,
260+ action : (
261+ < ArrowLink to = { heroActionTo } className = "whitespace-nowrap" >
262+ { heroActionLabel }
263+ </ ArrowLink >
264+ ) ,
265+ } }
266+ />
267+ </ div >
248268 )
249269}
250270
0 commit comments