Skip to content

Commit e95015f

Browse files
authored
fix(client): bypass client router for links explicitly specifying target (#2563)
BREAKING CHANGE: specifying `target="_self"` for internal links will now perform full reload.
1 parent 03855dd commit e95015f

File tree

7 files changed

+43
-21
lines changed

7 files changed

+43
-21
lines changed

docs/guide/asset-handling.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ There is one exception to this: if you have an HTML page in `public` and link to
3131
- [/pure.html](/pure.html)
3232
- <pathname:///pure.html>
3333

34+
Note that `pathname://` is only supported in Markdown links. Also, `pathname://` will open the link in a new tab by default. You can use `target="_self"` instead to open it in the same tab:
35+
36+
**Input**
37+
38+
```md
39+
[Link to pure.html](/pure.html){target="_self"}
40+
41+
<!-- there is no need to specify pathname:// if the target is explicitly specified -->
42+
```
43+
44+
**Output**
45+
46+
[Link to pure.html](/pure.html){target="_self"}
47+
3448
## Base URL
3549

3650
If your site is deployed to a non-root URL, you will need to set the `base` option in `.vitepress/config.js`. For example, if you plan to deploy your site to `https://foo.github.io/bar/`, then `base` should be set to `'/bar/'` (it should always start and end with a slash).

src/client/app/composables/preFetch.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function usePrefetch() {
6666
if (!hasFetched.has(pathname)) {
6767
hasFetched.add(pathname)
6868
const pageChunkPath = pathToFile(pathname)
69-
doFetch(pageChunkPath)
69+
if (pageChunkPath) doFetch(pageChunkPath)
7070
}
7171
}
7272
})
@@ -76,7 +76,6 @@ export function usePrefetch() {
7676
document
7777
.querySelectorAll<HTMLAnchorElement | SVGAElement>('#app a')
7878
.forEach((link) => {
79-
const { target } = link
8079
const { hostname, pathname } = new URL(
8180
link.href instanceof SVGAnimatedString
8281
? link.href.animVal
@@ -91,7 +90,7 @@ export function usePrefetch() {
9190
if (
9291
// only prefetch same tab navigation, since a new tab will load
9392
// the lean js chunk instead.
94-
target !== `_blank` &&
93+
link.target !== '_blank' &&
9594
// only prefetch inbound links
9695
hostname === location.hostname
9796
) {

src/client/app/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
1+
import RawTheme from '@theme/index'
12
import {
2-
type App,
33
createApp as createClientApp,
44
createSSRApp,
55
defineComponent,
66
h,
77
onMounted,
8-
watchEffect
8+
watchEffect,
9+
type App
910
} from 'vue'
10-
import RawTheme from '@theme/index'
11-
import { inBrowser, pathToFile } from './utils'
12-
import { type Router, RouterSymbol, createRouter, scrollTo } from './router'
13-
import { siteDataRef, useData } from './data'
14-
import { useUpdateHead } from './composables/head'
15-
import { usePrefetch } from './composables/preFetch'
16-
import { dataSymbol, initData } from './data'
17-
import { Content } from './components/Content'
1811
import { ClientOnly } from './components/ClientOnly'
19-
import { useCopyCode } from './composables/copyCode'
12+
import { Content } from './components/Content'
2013
import { useCodeGroups } from './composables/codeGroups'
14+
import { useCopyCode } from './composables/copyCode'
15+
import { useUpdateHead } from './composables/head'
16+
import { usePrefetch } from './composables/preFetch'
17+
import { dataSymbol, initData, siteDataRef, useData } from './data'
18+
import { RouterSymbol, createRouter, scrollTo, type Router } from './router'
19+
import { inBrowser, pathToFile } from './utils'
2120

2221
function resolveThemeExtends(theme: typeof RawTheme): typeof RawTheme {
2322
if (theme.extends) {
@@ -123,6 +122,8 @@ function newRouter(): Router {
123122
return createRouter((path) => {
124123
let pageFilePath = pathToFile(path)
125124

125+
if (!pageFilePath) return null
126+
126127
if (isInitialPageLoad) {
127128
initialPath = pageFilePath
128129
}

src/client/app/router.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const RouterSymbol: InjectionKey<Router> = Symbol()
2222

2323
// we are just using URL to parse the pathname and hash - the base doesn't
2424
// matter and is only passed to support same-host hrefs.
25-
const fakeHost = `http://a.com`
25+
const fakeHost = 'http://a.com'
2626

2727
const getDefaultRoute = (): Route => ({
2828
path: '/',
@@ -36,7 +36,7 @@ interface PageModule {
3636
}
3737

3838
export function createRouter(
39-
loadPageModule: (path: string) => Promise<PageModule>,
39+
loadPageModule: (path: string) => Awaitable<PageModule | null>,
4040
fallbackComponent?: Component
4141
): Router {
4242
const route = reactive(getDefaultRoute())
@@ -73,6 +73,9 @@ export function createRouter(
7373
const pendingPath = (latestPendingPath = targetLoc.pathname)
7474
try {
7575
let page = await loadPageModule(pendingPath)
76+
if (!page) {
77+
throw new Error(`Page not found: ${pendingPath}`)
78+
}
7679
if (latestPendingPath === pendingPath) {
7780
latestPendingPath = null
7881

@@ -120,7 +123,10 @@ export function createRouter(
120123
}
121124
}
122125
} catch (err: any) {
123-
if (!/fetch/.test(err.message) && !/^\/404(\.html|\/)?$/.test(href)) {
126+
if (
127+
!/fetch|Page not found/.test(err.message) &&
128+
!/^\/404(\.html|\/)?$/.test(href)
129+
) {
124130
console.error(err)
125131
}
126132

@@ -176,7 +182,7 @@ export function createRouter(
176182
!e.shiftKey &&
177183
!e.altKey &&
178184
!e.metaKey &&
179-
target !== `_blank` &&
185+
!target &&
180186
origin === currentUrl.origin &&
181187
// don't intercept if non-html extension is present
182188
!(extMatch && extMatch[0] !== '.html')

src/client/app/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export { inBrowser } from '../shared'
1818
/**
1919
* Join two paths by resolving the slash collision.
2020
*/
21-
export function joinPath(base: string, path: string): string {
21+
export function joinPath(base: string, path: string) {
2222
return `${base}${path}`.replace(/\/+/g, '/')
2323
}
2424

@@ -31,7 +31,7 @@ export function withBase(path: string) {
3131
/**
3232
* Converts a url path to the corresponding js chunk filename.
3333
*/
34-
export function pathToFile(path: string): string {
34+
export function pathToFile(path: string) {
3535
let pagePath = path.replace(/\.html$/, '')
3636
pagePath = decodeURIComponent(pagePath)
3737
pagePath = pagePath.replace(/\/$/, '/index') // /foo/ -> /foo/index
@@ -57,6 +57,7 @@ export function pathToFile(path: string): string {
5757
: pagePath.slice(0, -3) + '_index.md'
5858
pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()]
5959
}
60+
if (!pageHash) return null
6061
pagePath = `${base}assets/${pagePath}.${pageHash}.js`
6162
} else {
6263
// ssr build uses much simpler name mapping

src/client/theme-default/components/VPLocalSearchBox.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ debouncedWatch(
230230
async function fetchExcerpt(id: string) {
231231
const file = pathToFile(id.slice(0, id.indexOf('#')))
232232
try {
233+
if (!file) throw new Error(`Cannot find file for id: ${id}`)
233234
return { id, mod: await import(/*@vite-ignore*/ file) }
234235
} catch (e) {
235236
console.error(e)

src/client/theme-default/support/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function normalizeLink(url: string): string {
3333
}
3434

3535
const { site } = useData()
36-
const { pathname, search, hash } = new URL(url, 'http://example.com')
36+
const { pathname, search, hash } = new URL(url, 'http://a.com')
3737

3838
const normalizedPath =
3939
pathname.endsWith('/') || pathname.endsWith('.html')

0 commit comments

Comments
 (0)