From b260bca24f3c51a6e7e22b3da6291a536b73e1ce Mon Sep 17 00:00:00 2001 From: null Date: Thu, 10 Sep 2026 04:34:41 -0500 Subject: [PATCH] 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) --- src/components/ScrollToTop.jsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) 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