diff --git a/src/components/ScrollToTop.jsx b/src/components/ScrollToTop.jsx index 0f8ce0a..2947388 100644 --- a/src/components/ScrollToTop.jsx +++ b/src/components/ScrollToTop.jsx @@ -4,10 +4,23 @@ import { useLocation } from 'react-router-dom' export default function ScrollToTop() { 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(() => { - if (hash) { - const el = document.querySelector(hash) + if (hash.length > 1) { + let el = null + try { + el = document.getElementById(decodeURIComponent(hash.slice(1))) + } catch { + el = null // a fragment that is not valid percent-encoding + } if (el) { el.scrollIntoView({ behavior: 'smooth' }) return