Skip to content

Commit dcb6867

Browse files
committed
chore: remove hash symbol from internal links
INSTUI-4750
1 parent 7450ddc commit dcb6867

File tree

5 files changed

+103
-17
lines changed

5 files changed

+103
-17
lines changed

packages/__docs__/src/App/index.tsx

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class App extends Component<AppProps, AppState> {
185185
this._defaultDocumentTitle = document.title
186186
this.updateKey()
187187

188-
window.addEventListener('hashchange', this.updateKey, false)
188+
window.addEventListener('popstate', this.updateKey, false)
189189

190190
// TODO: Replace with the Responsive component later
191191
// Using this method directly for now instead to avoid a call to findDOMNode
@@ -225,6 +225,13 @@ class App extends Component<AppProps, AppState> {
225225
})
226226
})
227227
.catch(errorHandler)
228+
229+
const [page] = this.getPathInfo()
230+
const isHomepage = page === 'index' || typeof page === 'undefined'
231+
if (isHomepage) {
232+
this.setState({ showMenu: false })
233+
}
234+
228235
document.addEventListener('focusin', this.handleFocusChange)
229236
}
230237

@@ -242,7 +249,7 @@ class App extends Component<AppProps, AppState> {
242249
componentWillUnmount() {
243250
//cancel ongoing requests
244251
this._controller?.abort()
245-
window.removeEventListener('hashchange', this.updateKey, false)
252+
window.removeEventListener('popstate', this.updateKey, false)
246253

247254
if (this._mediaQueryListener) {
248255
this._mediaQueryListener.remove()
@@ -261,13 +268,51 @@ class App extends Component<AppProps, AppState> {
261268
}
262269

263270
getPathInfo = () => {
264-
const { hash } = window.location
271+
const { hash, pathname } = window.location
272+
273+
// Case 1: Old hash-based routing (hash contains the main content)
274+
const cleanPath = pathname.replace(/^\/+|\/+$/g, '')
275+
const pathSegments = cleanPath.split('/')
265276

266-
const path = hash && hash.split('/')
277+
// Check if the pathname is just a base path (ends with slash or has no meaningful final segment)
278+
const hasSubstantialPathname =
279+
pathSegments.length > 0 &&
280+
pathSegments[pathSegments.length - 1] !== '' &&
281+
!pathSegments.every((seg) => seg === '')
282+
283+
// If it's just a base path with no hash, treat as homepage
284+
if ((!hasSubstantialPathname || pathname.endsWith('/')) && !hash) {
285+
return ['index'] // homepage
286+
}
267287

268-
if (path) {
269-
const [page, id] = path.map((entry) => decodeURI(entry.replace('#', '')))
270-
return [page, id]
288+
if (
289+
hash &&
290+
(!hasSubstantialPathname || pathname.endsWith('/')) &&
291+
hash.startsWith('#') &&
292+
!hash.startsWith('##')
293+
) {
294+
const path = hash.split('/')
295+
if (path) {
296+
const [page, id] = path.map((entry) =>
297+
decodeURI(entry.replace('#', ''))
298+
)
299+
return [page, id]
300+
}
301+
}
302+
// Case 2: New clean URL routing (pathname contains the main content)
303+
else {
304+
if (pathSegments.length > 0 && pathSegments[0] !== '') {
305+
// Get the page from the last segment of the path
306+
const page = pathSegments[pathSegments.length - 1]
307+
// If there's a hash that's not at the beginning (like #Guidelines), use it as ID
308+
let id = undefined
309+
if (hash && hash.startsWith('##')) {
310+
id = decodeURI(hash.replace('##', ''))
311+
} else if (hash && !hash.startsWith('#/')) {
312+
id = decodeURI(hash.replace('#', ''))
313+
}
314+
return [page, id]
315+
}
271316
}
272317
return []
273318
}
@@ -289,7 +334,6 @@ class App extends Component<AppProps, AppState> {
289334

290335
updateKey = () => {
291336
const [page, _id] = this.getPathInfo()
292-
293337
if (page) {
294338
this.setState(
295339
({ key, showMenu }) => ({

packages/__docs__/src/Header/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,16 @@ class Header extends Component<HeaderProps> {
135135
const { versionsData } = this.props
136136
return (
137137
<View as="div" margin="none none medium" padding="none medium">
138-
<Link href="#index" isWithinText={false} display="block">
138+
<Link
139+
href="index"
140+
isWithinText={false}
141+
display="block"
142+
onClick={(e) => {
143+
e.preventDefault()
144+
window.history.pushState({}, '', '/')
145+
window.dispatchEvent(new PopStateEvent('popstate'))
146+
}}
147+
>
139148
<View display="block" textAlign="center">
140149
<InlineSVG src={logo} height="6rem" fontSize="12rem" />
141150
<ScreenReaderContent>Instructure logo</ScreenReaderContent>

packages/__docs__/src/Nav/index.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ class Nav extends Component<NavProps, NavState> {
141141
})
142142
}
143143

144+
getEffectiveBasePath = () => {
145+
const pathname = window.location.pathname
146+
// Check if we're in a PR preview (path contains /pr-preview/)
147+
const prPreviewMatch = pathname.match(/^(\/pr-preview\/pr-\d+)/)
148+
if (prPreviewMatch) {
149+
return prPreviewMatch[1]
150+
}
151+
// Live version - no base path
152+
return ''
153+
}
154+
144155
matchQuery(str: string): boolean {
145156
const { query } = this.state
146157
return query && typeof query.test === 'function' ? query.test(str) : true
@@ -257,6 +268,15 @@ class Nav extends Component<NavProps, NavState> {
257268
}
258269
}
259270

271+
handleInternalNavigation = (targetPath: string, e: React.MouseEvent) => {
272+
e.preventDefault()
273+
this.removeFocus(e as React.MouseEvent<ViewOwnProps>)
274+
const basePath = this.getEffectiveBasePath()
275+
const newUrl = basePath ? `${basePath}/${targetPath}` : `/${targetPath}`
276+
window.history.pushState({}, '', newUrl)
277+
window.dispatchEvent(new PopStateEvent('popstate'))
278+
}
279+
260280
renderDocLink(docId: string) {
261281
const { docs, selected } = this.props
262282
const docSelected = docId === selected
@@ -283,9 +303,9 @@ class Nav extends Component<NavProps, NavState> {
283303
/>
284304
)}
285305
<Link
286-
onClick={this.removeFocus}
306+
onClick={(e: any) => this.handleInternalNavigation(docId, e)}
287307
display="block"
288-
href={`#${docId}`}
308+
href={`/${docId}`} // Keep href simple
289309
isWithinText={false}
290310
>
291311
{docs[docId].title}
@@ -451,9 +471,9 @@ class Nav extends Component<NavProps, NavState> {
451471
>
452472
<Link
453473
display="block"
454-
onClick={this.removeFocus}
474+
onClick={(e: any) => this.handleInternalNavigation(themeKey, e)}
455475
isWithinText={false}
456-
href={`#${themeKey}`}
476+
href={`/${themeKey}`}
457477
>
458478
{themeKey}
459479
</Link>
@@ -479,9 +499,9 @@ class Nav extends Component<NavProps, NavState> {
479499
const themes = this.renderThemes()
480500
const icons = (
481501
<NavToggle
482-
key={'icons'}
483-
summary={'Icons'}
484-
href="#icons"
502+
summary="Icons"
503+
onToggle={(e: any) => this.handleInternalNavigation('icons', e)}
504+
href="icons"
485505
shouldBlur={true}
486506
/>
487507
)

packages/__docs__/src/compileMarkdown.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,19 @@ const renderer = (title: string) => ({
272272
heading: (text: string, level: number) =>
273273
headingVariants[`h${level}`]?.(uuid(), text),
274274
link: (href: string, text: string) => (
275-
<Link key={uuid()} href={href}>
275+
<Link
276+
key={uuid()}
277+
href={href}
278+
onClick={(e: any) => {
279+
e.preventDefault()
280+
const basePath =
281+
window.location.pathname.match(/^(\/pr-preview\/pr-\d+)/)?.[1] || ''
282+
const newUrl = basePath ? `${basePath}/${href}` : `/${href}`
283+
window.history.pushState({}, '', newUrl)
284+
window.dispatchEvent(new PopStateEvent('popstate'))
285+
}}
286+
isWithinText={false}
287+
>
276288
{text}
277289
</Link>
278290
)

packages/__docs__/webpack.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const config = merge(baseConfig, {
5959
directory: outputPath,
6060
},
6161
host: '0.0.0.0',
62+
historyApiFallback: true,
6263
onListening: function () {
6364
// devServer is watching source files by default and hot reloading the docs page if they are changed
6465
// however markdown files (i.e. README.md) need to be recompiled hence the need for chokidar

0 commit comments

Comments
 (0)