tfrere HF Staff Cursor commited on
Commit
800eea1
·
1 Parent(s): 638248e

feat: switch to BrowserRouter with HF iframe deep linking support

Browse files

Replace HashRouter with BrowserRouter for clean URLs. Add HashRedirect
to handle hash-to-path conversion on initial iframe load (HF propagates
parent hash to iframe). Add RouteSync to sync current route back to
parent page via postMessage so users can copy/share deep links.

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (1) hide show
  1. src/App.jsx +51 -10
src/App.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import { useEffect } from 'react';
2
- import { HashRouter, Routes, Route, useLocation } from 'react-router-dom';
3
  import { ThemeProvider, CssBaseline } from '@mui/material';
4
  import theme from './theme/theme';
5
  import { AppsProvider } from './context/AppsContext';
@@ -13,14 +13,57 @@ import Buy from './pages/Buy';
13
  import GettingStarted from './pages/GettingStarted';
14
  import Build from './pages/Build';
15
 
16
- function ScrollToTop() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  const location = useLocation();
18
 
19
  useEffect(() => {
20
- // Check for scrollTo query parameter (used for anchor-like behavior with HashRouter)
 
 
 
 
 
 
 
 
 
 
 
 
21
  const params = new URLSearchParams(location.search);
22
  const scrollTo = params.get('scrollTo');
23
-
24
  if (scrollTo) {
25
  // Retry mechanism to wait for element to be rendered
26
  const scrollToElement = (retries = 0) => {
@@ -28,14 +71,11 @@ function ScrollToTop() {
28
  if (element) {
29
  element.scrollIntoView({ behavior: 'smooth', block: 'center' });
30
  } else if (retries < 10) {
31
- // Retry up to 10 times with 100ms interval
32
  setTimeout(() => scrollToElement(retries + 1), 100);
33
  }
34
  };
35
- // Initial delay for page render
36
  setTimeout(() => scrollToElement(), 300);
37
  } else {
38
- // Otherwise scroll to top
39
  window.scrollTo({ top: 0, behavior: 'smooth' });
40
  }
41
  }, [location.pathname, location.search]);
@@ -49,8 +89,9 @@ export default function App() {
49
  <CssBaseline />
50
  <AuthProvider>
51
  <AppsProvider>
52
- <HashRouter>
53
- <ScrollToTop />
 
54
  <Routes>
55
  <Route path="/" element={<Home />} />
56
  <Route path="/getting-started" element={<GettingStarted />} />
@@ -60,7 +101,7 @@ export default function App() {
60
  <Route path="/apps" element={<Apps />} />
61
  <Route path="/buy" element={<Buy />} />
62
  </Routes>
63
- </HashRouter>
64
  </AppsProvider>
65
  </AuthProvider>
66
  </ThemeProvider>
 
1
  import { useEffect } from 'react';
2
+ import { BrowserRouter, Routes, Route, useLocation, useNavigate } from 'react-router-dom';
3
  import { ThemeProvider, CssBaseline } from '@mui/material';
4
  import theme from './theme/theme';
5
  import { AppsProvider } from './context/AppsContext';
 
13
  import GettingStarted from './pages/GettingStarted';
14
  import Build from './pages/Build';
15
 
16
+ /**
17
+ * Handle hash-to-path redirect for HuggingFace Spaces iframe embedding.
18
+ *
19
+ * HF propagates the parent page's hash to the iframe on initial load.
20
+ * For example, visiting huggingface.co/reachy-mini#/apps will load the
21
+ * iframe at *.hf.space/#/apps. This component reads that hash and
22
+ * converts it to a BrowserRouter path (e.g. /apps).
23
+ */
24
+ function HashRedirect() {
25
+ const navigate = useNavigate();
26
+
27
+ useEffect(() => {
28
+ const hash = window.location.hash;
29
+ // Match hash routes like #/apps, #/download, etc.
30
+ if (hash && hash.startsWith('#/')) {
31
+ const path = hash.slice(1); // Remove the # to get /apps
32
+ window.location.hash = ''; // Clean up the hash
33
+ navigate(path, { replace: true });
34
+ }
35
+ }, [navigate]);
36
+
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Sync the current route back to the HF parent page via postMessage.
42
+ * This updates the URL in the browser address bar so users can
43
+ * copy/share deep links (e.g. huggingface.co/reachy-mini#/apps).
44
+ *
45
+ * Also handles scrollTo query parameter for anchor-like behavior.
46
+ */
47
+ function RouteSync() {
48
  const location = useLocation();
49
 
50
  useEffect(() => {
51
+ // Sync current path to parent frame hash (for HF Spaces embedding)
52
+ const isInIframe = window.parent !== window;
53
+ if (isInIframe && location.pathname !== '/') {
54
+ window.parent.postMessage(
55
+ { hash: `#${location.pathname}` },
56
+ 'https://huggingface.co'
57
+ );
58
+ } else if (isInIframe && location.pathname === '/') {
59
+ // Clear hash when on home page
60
+ window.parent.postMessage({ hash: '' }, 'https://huggingface.co');
61
+ }
62
+
63
+ // Handle scrollTo query parameter
64
  const params = new URLSearchParams(location.search);
65
  const scrollTo = params.get('scrollTo');
66
+
67
  if (scrollTo) {
68
  // Retry mechanism to wait for element to be rendered
69
  const scrollToElement = (retries = 0) => {
 
71
  if (element) {
72
  element.scrollIntoView({ behavior: 'smooth', block: 'center' });
73
  } else if (retries < 10) {
 
74
  setTimeout(() => scrollToElement(retries + 1), 100);
75
  }
76
  };
 
77
  setTimeout(() => scrollToElement(), 300);
78
  } else {
 
79
  window.scrollTo({ top: 0, behavior: 'smooth' });
80
  }
81
  }, [location.pathname, location.search]);
 
89
  <CssBaseline />
90
  <AuthProvider>
91
  <AppsProvider>
92
+ <BrowserRouter>
93
+ <HashRedirect />
94
+ <RouteSync />
95
  <Routes>
96
  <Route path="/" element={<Home />} />
97
  <Route path="/getting-started" element={<GettingStarted />} />
 
101
  <Route path="/apps" element={<Apps />} />
102
  <Route path="/buy" element={<Buy />} />
103
  </Routes>
104
+ </BrowserRouter>
105
  </AppsProvider>
106
  </AuthProvider>
107
  </ThemeProvider>