Skip to content

Commit 4d034b7

Browse files
feat: implement static routing enhancements for demo applications
- Introduced a new static site routing system to improve navigation for demo applications, ensuring seamless transitions and preventing 404 errors. - Refactored the VitePress theme to utilize the new routing system, enhancing user experience when accessing demo content. - Updated site navigation to include the installation of demo static routing, improving overall functionality and consistency across the application. - Adjusted the generated timestamp in eval-results-all.json to reflect the latest generation date.
1 parent e5538b4 commit 4d034b7

5 files changed

Lines changed: 203 additions & 64 deletions

File tree

demo/public/eval-results-all.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"generatedAt": "2026-06-17T11:29:22.331Z",
2+
"generatedAt": "2026-06-17T11:41:43.836Z",
33
"methodology": "multistep",
44
"passRule": "unified",
55
"passRuleNote": "All approaches pass when correctFinalAction && !hitDecoy && flowCoverage >= minCoverage. flowCoverage recomputed from archived rawResponse with current flow-coverage.ts (no API re-run).",

demo/site-nav.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
resolveDocsBase,
1111
type SitePage,
1212
} from '../shared/site-nav';
13+
import { installDemoStaticRouting } from '../shared/static-site-routing';
1314
import { initMobileNav } from './nav-mobile';
1415

1516
function escapeHtml(value: string): string {
@@ -206,6 +207,7 @@ export function initSiteNav(page: SitePage): void {
206207
mount.classList.remove('site-header-mount');
207208
mount.removeAttribute('aria-busy');
208209
patchDocsLinks();
210+
installDemoStaticRouting();
209211
initDesktopDropdowns();
210212
initMobileNav();
211213
syncHeaderOffset(mount);

docs/.vitepress/theme/index.ts

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,11 @@
11
import type { Theme } from 'vitepress';
22
import DefaultTheme from 'vitepress/theme';
3-
import { isDemoAppPath, toDemoAppUrl } from '../../../shared/site-nav';
3+
import { installVitePressStaticRouting } from '../../../shared/static-site-routing';
44
import './custom.css';
55

6-
function navigateToDemoApp(href: string): boolean {
7-
let url: URL;
8-
try {
9-
url = new URL(href, window.location.origin);
10-
} catch {
11-
return false;
12-
}
13-
if (!isDemoAppPath(url.pathname)) return false;
14-
window.location.assign(toDemoAppUrl(url.pathname, url.hash));
15-
return true;
16-
}
17-
186
export default {
197
extends: DefaultTheme,
20-
enhanceApp({ router }) {
21-
if (typeof window === 'undefined') return;
22-
23-
// VitePress client routing treats /demo/* as in-app routes and shows its 404.
24-
// Hard-navigate so Vercel/static hosting serves the Vite-built demo HTML.
25-
router.beforeEach((to) => {
26-
if (!isDemoAppPath(to.path)) return;
27-
window.location.assign(toDemoAppUrl(to.path, to.hash));
28-
return false;
29-
});
30-
31-
window.addEventListener(
32-
'click',
33-
(event) => {
34-
if (event.defaultPrevented || event.button !== 0) return;
35-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
36-
37-
const anchor = (event.target as Element | null)?.closest('a');
38-
if (!anchor) return;
39-
40-
const href = anchor.getAttribute('href');
41-
if (!href || href.startsWith('#')) return;
42-
43-
if (navigateToDemoApp(href)) {
44-
event.preventDefault();
45-
event.stopPropagation();
46-
}
47-
},
48-
true
49-
);
8+
enhanceApp({ router, siteData }) {
9+
installVitePressStaticRouting(router, siteData.value.base);
5010
},
5111
} satisfies Theme;

shared/site-nav.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -106,26 +106,11 @@ export function resolveDemoBase(): string {
106106
return docsBase.endsWith('/') ? `${docsBase}demo/` : `${docsBase}/demo/`;
107107
}
108108

109-
/** VitePress SPA routes that must hard-navigate to static demo HTML (not VitePress pages). */
110-
const DEMO_SPA_TRAP_PATHS = new Set(['/demo', '/demo/index', '/demo/scenarios']);
111-
112-
/** True when VitePress client routing would 404 instead of loading the static demo app. */
113-
export function isDemoAppPath(pathname: string): boolean {
114-
const path = pathname.replace(/\/+$/, '') || '/';
115-
if (path.endsWith('.html')) return false;
116-
return DEMO_SPA_TRAP_PATHS.has(path);
117-
}
118-
119-
/**
120-
* Map demo paths to static HTML files on the combined docs+demo deployment.
121-
* VitePress `cleanUrls` can normalize `/demo/scenarios.html` → `/demo/scenarios`, which 404s in the SPA.
122-
*/
123-
export function toDemoAppUrl(pathname: string, hash = ''): string {
124-
const path = pathname.replace(/\/+$/, '') || '/';
125-
if (path === '/demo' || path === '/demo/index') return `/demo/index.html${hash}`;
126-
if (path === '/demo/scenarios') return `/demo/scenarios.html${hash}`;
127-
return `${pathname}${hash}`;
128-
}
109+
export {
110+
isDemoAppPath,
111+
resolveDemoEntryUrl,
112+
toDemoAppUrl,
113+
} from './static-site-routing';
129114

130115
export function hrefForDemoItem(item: NavLink): string {
131116
if (item.external) return item.docsHref;

shared/static-site-routing.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* Cross-app routing for the combined VitePress docs + static Vite demo deployment.
3+
* VitePress intercepts same-origin HTML links for client-side navigation; paths that
4+
* are not VitePress pages (demo app, scenario HTML, static assets) must hard-navigate.
5+
*/
6+
7+
export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
8+
9+
declare global {
10+
interface Window {
11+
__VP_HASH_MAP__?: Record<string, string>;
12+
}
13+
}
14+
15+
const DEMO_ENTRY_PATHS = new Set([
16+
'/demo',
17+
'/demo/index',
18+
'/demo/index.html',
19+
'/demo/scenarios',
20+
'/demo/scenarios.html',
21+
]);
22+
23+
function sanitizeVpFileName(name: string): string {
24+
return name
25+
.replace(/[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g, '_')
26+
.replace(/(^|\/)_+(?=[^/]*$)/, '$1');
27+
}
28+
29+
/** True when the path is served by the static Vite demo (not VitePress). */
30+
export function isStaticAppPath(pathname: string): boolean {
31+
const path = pathname.replace(/\/+$/, '') || '/';
32+
return path === '/demo' || path.startsWith('/demo/');
33+
}
34+
35+
/** Canonical demo entry URLs (index / scenarios). */
36+
export function resolveDemoEntryUrl(pathname: string, hash = ''): string | null {
37+
const path = pathname.replace(/\/+$/, '') || '/';
38+
if (!DEMO_ENTRY_PATHS.has(path)) return null;
39+
if (path === '/demo/scenarios' || path === '/demo/scenarios.html') {
40+
return `/demo/scenarios.html${hash}`;
41+
}
42+
return `/demo/index.html${hash}`;
43+
}
44+
45+
export function isDemoAppPath(pathname: string): boolean {
46+
return resolveDemoEntryUrl(pathname) !== null;
47+
}
48+
49+
export function toDemoAppUrl(pathname: string, hash = ''): string {
50+
return resolveDemoEntryUrl(pathname, hash) ?? `${pathname}${hash}`;
51+
}
52+
53+
/** Mirrors VitePress `pathToFile` hash-map lookup (production client build). */
54+
export function isVitePressPagePath(pathname: string, base = '/'): boolean {
55+
if (typeof window === 'undefined') return false;
56+
const hashMap = window.__VP_HASH_MAP__;
57+
if (!hashMap) return false;
58+
59+
let pagePath = pathname.replace(/\.html$/, '');
60+
try {
61+
pagePath = decodeURIComponent(pagePath);
62+
} catch {
63+
/* keep raw path */
64+
}
65+
pagePath = pagePath.replace(/\/$/, '/index');
66+
67+
let mdKey =
68+
sanitizeVpFileName(pagePath.slice(base.length).replace(/\//g, '_') || 'index') + '.md';
69+
70+
if (hashMap[mdKey.toLowerCase()]) return true;
71+
72+
mdKey = mdKey.endsWith('_index.md')
73+
? mdKey.slice(0, -9) + '.md'
74+
: mdKey.slice(0, -3) + '_index.md';
75+
76+
return Boolean(hashMap[mdKey.toLowerCase()]);
77+
}
78+
79+
/**
80+
* When non-null, the URL must be loaded with a full navigation (not VitePress SPA).
81+
*/
82+
export function resolveHardNavigationTarget(href: string, base = '/'): string | null {
83+
if (!href || href.startsWith('#') || EXTERNAL_URL_RE.test(href)) return null;
84+
85+
let url: URL;
86+
try {
87+
url = new URL(href, window.location.origin);
88+
} catch {
89+
return null;
90+
}
91+
92+
if (url.origin !== window.location.origin) return null;
93+
94+
const demoEntry = resolveDemoEntryUrl(url.pathname, url.hash);
95+
if (demoEntry) return demoEntry;
96+
97+
const staticTarget = `${url.pathname}${url.search}${url.hash}`;
98+
99+
if (isStaticAppPath(url.pathname)) return staticTarget;
100+
101+
if (!isVitePressPagePath(url.pathname, base)) return staticTarget;
102+
103+
return null;
104+
}
105+
106+
interface VitePressRouterHooks {
107+
onBeforeRouteChange?: (href: string) => unknown;
108+
onBeforePageLoad?: (href: string) => unknown;
109+
}
110+
111+
/** Wire hard-navigation for all non-VitePress routes in the docs SPA. */
112+
export function installVitePressStaticRouting(
113+
router: VitePressRouterHooks,
114+
base = '/'
115+
): void {
116+
if (typeof window === 'undefined') return;
117+
118+
const hardNavigate = (href: string): boolean => {
119+
const target = resolveHardNavigationTarget(href, base);
120+
if (!target) return false;
121+
window.location.assign(target);
122+
return true;
123+
};
124+
125+
const previousOnBeforeRouteChange = router.onBeforeRouteChange;
126+
router.onBeforeRouteChange = async (href) => {
127+
if (hardNavigate(href)) return false;
128+
return previousOnBeforeRouteChange?.(href);
129+
};
130+
131+
const previousOnBeforePageLoad = router.onBeforePageLoad;
132+
router.onBeforePageLoad = async (href) => {
133+
if (hardNavigate(href)) return false;
134+
return previousOnBeforePageLoad?.(href);
135+
};
136+
137+
window.addEventListener(
138+
'click',
139+
(event) => {
140+
if (event.defaultPrevented || event.button !== 0) return;
141+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
142+
143+
const anchor = (event.target as Element | null)?.closest('a');
144+
if (!anchor || anchor.hasAttribute('download') || anchor.hasAttribute('target')) return;
145+
146+
const linkHref = anchor.getAttribute('href');
147+
if (!linkHref || linkHref.startsWith('#')) return;
148+
149+
if (hardNavigate(linkHref)) {
150+
event.preventDefault();
151+
event.stopImmediatePropagation();
152+
}
153+
},
154+
true
155+
);
156+
}
157+
158+
/** Canonical demo entry links on static demo pages (index / scenarios). */
159+
export function installDemoStaticRouting(): void {
160+
if (typeof window === 'undefined') return;
161+
162+
window.addEventListener(
163+
'click',
164+
(event) => {
165+
if (event.defaultPrevented || event.button !== 0) return;
166+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
167+
168+
const anchor = (event.target as Element | null)?.closest('a');
169+
if (!anchor || anchor.hasAttribute('download') || anchor.hasAttribute('target')) return;
170+
171+
const href = anchor.getAttribute('href');
172+
if (!href || href.startsWith('#')) return;
173+
174+
let url: URL;
175+
try {
176+
url = new URL(href, window.location.href);
177+
} catch {
178+
return;
179+
}
180+
181+
if (url.origin !== window.location.origin) return;
182+
183+
const demoTarget = resolveDemoEntryUrl(url.pathname, url.hash);
184+
if (!demoTarget) return;
185+
186+
event.preventDefault();
187+
event.stopImmediatePropagation();
188+
window.location.assign(`${demoTarget}${url.search}`);
189+
},
190+
true
191+
);
192+
}

0 commit comments

Comments
 (0)