From 80cf2650aa5fa4b49093509f60766cc5b28c19bc Mon Sep 17 00:00:00 2001 From: T <6601329+cookesan@users.noreply.github.com> Date: Sun, 28 Jun 2026 07:23:29 -0300 Subject: [PATCH 01/29] fix: index rewritten local search pages by locale (#5241) --- .../node/plugins/localSearchPlugin.test.ts | 102 ++++++++++++++++++ src/node/plugins/localSearchPlugin.ts | 5 +- 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/node/plugins/localSearchPlugin.test.ts diff --git a/__tests__/unit/node/plugins/localSearchPlugin.test.ts b/__tests__/unit/node/plugins/localSearchPlugin.test.ts new file mode 100644 index 00000000..c71d628f --- /dev/null +++ b/__tests__/unit/node/plugins/localSearchPlugin.test.ts @@ -0,0 +1,102 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { resolveConfig } from 'node/config' +import { localSearchPlugin } from 'node/plugins/localSearchPlugin' +import MiniSearch from 'minisearch' + +describe('node/plugins/localSearchPlugin', () => { + let root: string | undefined + let nodeEnv: string | undefined + + beforeEach(() => { + nodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + }) + + afterEach(async () => { + if (nodeEnv === undefined) { + delete process.env.NODE_ENV + } else { + process.env.NODE_ENV = nodeEnv + } + + if (root) { + await rm(root, { recursive: true, force: true }) + root = undefined + } + }) + + test('indexes rewritten pages by rewritten locale path', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-local-search-')) + const configDir = path.join(root, '.vitepress') + await mkdir(configDir) + + await writeFile( + path.join(root, 'index.md'), + '# English home\n\nrootonlytoken\n' + ) + await writeFile( + path.join(root, 'zh.md'), + '# Chinese home\n\nlocaleonlytoken\n' + ) + await writeFile( + path.join(configDir, 'config.ts'), + [ + 'export default {', + ' rewrites: {', + " 'index.md': 'guide.md',", + " 'zh.md': 'zh/guide.md'", + ' },', + ' locales: {', + " root: { label: 'English', lang: 'en' },", + " zh: { label: 'Chinese', lang: 'zh' }", + ' },', + ' themeConfig: {', + " search: { provider: 'local' }", + ' }', + '}' + ].join('\n') + ) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const plugin = await localSearchPlugin(siteConfig) + + const indexModule = (await plugin.load?.call( + {} as never, + '/@localSearchIndex' + )) as string + + expect(indexModule).toContain( + '"root": () => import(\'@localSearchIndexroot\')' + ) + expect(indexModule).toContain('"zh": () => import(\'@localSearchIndexzh\')') + + const rootIndex = loadIndex( + (await plugin.load?.call({} as never, '/@localSearchIndexroot')) as string + ) + const zhIndex = loadIndex( + (await plugin.load?.call({} as never, '/@localSearchIndexzh')) as string + ) + + expect(rootIndex.search('rootonlytoken')).toMatchObject([ + { id: '/guide.html#english-home' } + ]) + expect(rootIndex.search('localeonlytoken')).toEqual([]) + + expect(zhIndex.search('localeonlytoken')).toMatchObject([ + { id: '/zh/guide.html#chinese-home' } + ]) + expect(zhIndex.search('rootonlytoken')).toEqual([]) + }) +}) + +function loadIndex(serializedModule: string) { + const serializedIndex = JSON.parse( + serializedModule.slice('export default '.length) + ) + return MiniSearch.loadJSON(serializedIndex, { + fields: ['title', 'titles', 'text'], + storeFields: ['title', 'titles'] + }) +} diff --git a/src/node/plugins/localSearchPlugin.ts b/src/node/plugins/localSearchPlugin.ts index 0af42dac..3c260e20 100644 --- a/src/node/plugins/localSearchPlugin.ts +++ b/src/node/plugins/localSearchPlugin.ts @@ -116,7 +116,10 @@ export async function localSearchPlugin( const file = path.join(siteConfig.srcDir, page) // get file metadata const fileId = getDocId(file) - const locale = getLocaleForPath(siteConfig.site, page) + const locale = getLocaleForPath( + siteConfig.site, + siteConfig.rewrites.map[page] || page + ) const index = getIndexByLocale(locale) // retrieve file and split into "sections" const html = await render(file) From d0159c8a850cbd2a010a3d44bdeb97c3db651d0e Mon Sep 17 00:00:00 2001 From: T <6601329+cookesan@users.noreply.github.com> Date: Sun, 28 Jun 2026 07:23:59 -0300 Subject: [PATCH 02/29] feat: support social link target option (#5242) --- __tests__/e2e/.vitepress/config.ts | 8 ++++++++ __tests__/e2e/home.test.ts | 9 +++++++++ docs/en/reference/default-theme-config.md | 2 ++ src/client/theme-default/components/VPSocialLink.vue | 3 ++- src/client/theme-default/components/VPSocialLinks.vue | 3 ++- types/default-theme.d.ts | 1 + 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts index 121761e7..441eda1c 100644 --- a/__tests__/e2e/.vitepress/config.ts +++ b/__tests__/e2e/.vitepress/config.ts @@ -162,6 +162,14 @@ export default defineConfig({ themeConfig: { nav, sidebar, + socialLinks: [ + { + icon: 'github', + link: '/home', + ariaLabel: 'Home social link', + target: '_self' + } + ], search: { provider: 'local', options: { diff --git a/__tests__/e2e/home.test.ts b/__tests__/e2e/home.test.ts index 76868361..b6bd6ff9 100644 --- a/__tests__/e2e/home.test.ts +++ b/__tests__/e2e/home.test.ts @@ -32,4 +32,13 @@ describe('render correct content', async () => { const outlineLinksCount = await outlineLinksLocator.count() expect(outlineLinksCount).toEqual(4) }) + + test('social link target override', async () => { + const socialLink = page.locator( + '.VPNavBarSocialLinks a[aria-label="Home social link"]' + ) + + expect(await socialLink.getAttribute('href')).toBe('/home') + expect(await socialLink.getAttribute('target')).toBe('_self') + }) }) diff --git a/docs/en/reference/default-theme-config.md b/docs/en/reference/default-theme-config.md index b515cb45..cc4ffd0e 100644 --- a/docs/en/reference/default-theme-config.md +++ b/docs/en/reference/default-theme-config.md @@ -253,6 +253,7 @@ export default { // You can add any icon from simple-icons (https://simpleicons.org/): { icon: 'github', link: 'https://github.com/vuejs/vitepress' }, { icon: 'twitter', link: '...' }, + { icon: 'discord', link: '/community', target: '_self' }, // You can also add custom icons by passing SVG as string: { icon: { @@ -272,6 +273,7 @@ interface SocialLink { icon: string | { svg: string } link: string ariaLabel?: string + target?: string } ``` diff --git a/src/client/theme-default/components/VPSocialLink.vue b/src/client/theme-default/components/VPSocialLink.vue index 2a016462..7e441ebb 100644 --- a/src/client/theme-default/components/VPSocialLink.vue +++ b/src/client/theme-default/components/VPSocialLink.vue @@ -7,6 +7,7 @@ const props = defineProps<{ icon: DefaultTheme.SocialLinkIcon link: string ariaLabel?: string + target?: string me: boolean }>() @@ -45,7 +46,7 @@ if (import.meta.env.SSR) { class="VPSocialLink no-icon" :href="link" :aria-label="ariaLabel ?? (typeof icon === 'string' ? icon : '')" - target="_blank" + :target="target ?? '_blank'" :rel="me ? 'me noopener' : 'noopener'" v-html="svg" > diff --git a/src/client/theme-default/components/VPSocialLinks.vue b/src/client/theme-default/components/VPSocialLinks.vue index b188fb89..062f7f62 100644 --- a/src/client/theme-default/components/VPSocialLinks.vue +++ b/src/client/theme-default/components/VPSocialLinks.vue @@ -13,11 +13,12 @@ withDefaults(defineProps<{