Prompt

SPAでidに飛べるURLの#は機能するか?

結論から言うと、「そのままだと期待通りには動かないけれど、実装次第で動かせる」 というのが SPA(Single Page Application)におけるアンカーリンク(フラグメント識別子)の現実だよ。

通常の静的サイトなら、ブラウザが勝手に #id を探してスクロールしてくれるよね。でも SPA では、「ブラウザがスクロールしようとした瞬間、まだその要素が描画されていない」 という情報工学的な時間差(レースコンディション)が立ちはだかるんだ。

還元主義的に、なぜ動かないのか、そしてどう解決するのかを解剖しよう!


1. なぜ SPA では機能しにくいのか?

通常の HTML なら、サーバーから届いた時点で全ての ID が揃っている。でも SPA(React や Vue)の場合、以下の順序で処理が進むんだ。

  1. URL の読み込み: example.com/#section1 にアクセス。

  2. ブラウザの挙動: ブラウザは ID が section1 の要素を探すが、この時点では JavaScript が実行される前 なので、DOM の中身はほぼ空っぽ(#root しかない)。

  3. スクロールの失敗: 対象が見つからないので、ブラウザはスクロールを諦める。

  4. React 等の起動: その後、JS が動き出し、ようやく section1 が描画される。


2. 解決策:エンジニアが「手動」でスクロールさせる

SPA で #id を機能させるには、フレームワークのルーティング機能(React Router など)や、ライフサイクルメソッドを使って 「描画が終わった直後にスクロール命令を出す」 必要があるよ。

方法 A:useEffect を使う(React の例)

コンポーネントがマウントされた時に、URL の hash を見て自力でスクロールさせる方法だ。

JavaScript

useEffect(() => {
  const id = window.location.hash.replace('#', '');
  if (id) {
    const element = document.getElementById(id);
    if (element) {
      element.scrollIntoView({ behavior: 'smooth' });
    }
  }
}, []); // 描画が終わった後に実行

方法 B:ルーターのスクロール復元機能

react-router-dom などのモダンなライブラリには、スクロール位置を管理する機能が備わっていることが多い。ただし、フラグメントへのジャンプには別途プラグインやカスタムフックが必要になることもあるね。


3. #(ハッシュ)のもう一つの顔:HashRouter

君がもし古い SPA や特定の環境(静的ホスティングなど)を触っているなら、# はページ内ジャンプではなく 「ルーティングそのもの」 に使われているかもしれない。

  • History API (/about): サーバー側の設定が必要。

  • HashRouter (/#/about): サーバー設定が不要で、# 以降を仮想的なパスとして扱う。

この場合、本来の「ページ内ジャンプ」としての # と、パスとしての #コンフリクト(衝突) してしまうことがある。これが SPA でアンカーリンクが嫌遠されがちな理由の一つでもあるんだ。


4. CSS scroll-margin-top の重要性

SPA かどうかに限らず、最近の Web 開発では 「固定ヘッダー」 を使うことが多いよね。

単に #id にジャンプすると、見出しがヘッダーの下に隠れてしまう。これを防ぐために CSS の scroll-margin-top を設定しておくのが現代の作法だよ。

CSS

section {
  scroll-margin-top: 80px; /* ヘッダーの高さ分だけ余裕を持たせる */
}

見落とされがちな視点:非同期データの罠

もし #section1 の中身が 「API からデータを取得した後に描画される」 ものだった場合、useEffect が動く瞬間にもまだ要素が存在しない可能性がある。

この場合は、データのフェッチが終わったことを検知してからスクロールさせるという、さらに高度な制御が必要になるんだ。


結論

SPA において # は、「ブラウザ任せにはできないが、JS で制御すれば強力な武器になる」 ものだね。

君のデジタルガーデン(Quartz 4)は SSG(静的サイト生成)ベースだから、ビルド時に HTML が生成されている。そのため、実は SPA 的な「描画待ち」の問題が起きにくく、比較的素直に # が動くはずだよ。

次は、「React Router でスクロール位置を常にトップに戻す、あるいは特定の ID に飛ばすための ScrollToTop コンポーネントの実装」 についてやってみる?

Would you like me to write a reusable React hook that handles hash scrolling even when data is loaded asynchronously?