fix(ui): a link with an odd fragment took the whole page down
document.querySelector THROWS on a fragment that is not a valid CSS selector,
and an id starting with a digit is not one. The call sat in an effect inside the
root route, so the error boundary replaced the header, the page and the footer
with the error screen. Live today: /services#1 blanks the page. Confirmed in
Chromium, where querySelector('#1') raises a DOMException.
That matters now because the long-form service pages exist to be deep-linked
from search results and AI answers, and two of their sections are about 8x8. A
link nobody here wrote must not be able to blank a page.
getElementById takes an id rather than a selector, so it cannot throw on one,
and the decode is wrapped for a fragment that is not valid percent-encoding.
An unknown id scrolls to the top, as before.
Proven in a real browser against the built site: /services#1, /#%E0 and
/services#8x8-implementation all render with header, footer and h1 intact and no
page error, and /contact#contact-form still scrolls to the form.
That run also confirmed at runtime what #226 suspected from reading the code:
every page logs React error #418, a hydration mismatch. It is pre-existing and
unrelated to this change, and it is fixed next.
Closes #231.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28b07abc28
commit
b260bca24f
|
|
@ -4,10 +4,23 @@ import { useLocation } from 'react-router-dom'
|
||||||
export default function ScrollToTop() {
|
export default function ScrollToTop() {
|
||||||
const { pathname, hash } = useLocation()
|
const { pathname, hash } = useLocation()
|
||||||
|
|
||||||
// Cross-page navigation: scroll to hash or top on route change
|
// Cross-page navigation: scroll to hash or top on route change.
|
||||||
|
//
|
||||||
|
// getElementById, not querySelector. querySelector THROWS on a fragment that
|
||||||
|
// is not a valid CSS selector, and an id starting with a digit is not one:
|
||||||
|
// `#8x8-implementation` and even `/#1` would throw here, in an effect inside
|
||||||
|
// the root route, and the error boundary would replace the header, the page
|
||||||
|
// and the footer with the error screen. The long-form service pages exist to
|
||||||
|
// be deep-linked from search results and AI answers, so a link nobody here
|
||||||
|
// wrote must never be able to blank the page. An unknown id scrolls to top.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hash) {
|
if (hash.length > 1) {
|
||||||
const el = document.querySelector(hash)
|
let el = null
|
||||||
|
try {
|
||||||
|
el = document.getElementById(decodeURIComponent(hash.slice(1)))
|
||||||
|
} catch {
|
||||||
|
el = null // a fragment that is not valid percent-encoding
|
||||||
|
}
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollIntoView({ behavior: 'smooth' })
|
el.scrollIntoView({ behavior: 'smooth' })
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue