Skip to content

Commit bddb7d0

Browse files
committed
feat: path-based page routing instead of query params
- Pages now use URL paths: /usage, /copilot, /files, etc. - Root / defaults to copilot page - Old ?page= URLs still work (backward compat) - Added 404.html SPA fallback for GitHub Pages - Added sessionStorage redirect restore in index.html - Sidebar nav links use real paths for accessibility - Other state (groupBy, tab, filters) stays as query params
1 parent 8c03bdc commit bddb7d0

4 files changed

Lines changed: 70 additions & 12 deletions

File tree

index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@
9797
-moz-osx-font-smoothing: grayscale;
9898
}
9999
</style>
100+
101+
<!-- SPA redirect restore: GitHub Pages 404.html saves the original path -->
102+
<script>
103+
(function() {
104+
var redirect = sessionStorage.getItem('spa-redirect');
105+
if (redirect) {
106+
sessionStorage.removeItem('spa-redirect');
107+
history.replaceState(null, '', redirect);
108+
}
109+
})();
110+
</script>
100111
</head>
101112
<body>
102113
<noscript>

public/404.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Redirecting...</title>
6+
<script>
7+
// GitHub Pages SPA fallback: save the path so index.html can restore it
8+
sessionStorage.setItem('spa-redirect', window.location.pathname + window.location.search + window.location.hash);
9+
// Redirect to the app root — BASE_PATH is injected at build time, but since
10+
// this is a static file we derive it from the repo path (first two segments)
11+
var segments = window.location.pathname.split('/');
12+
var base = '/' + (segments[1] || '') + '/';
13+
window.location.replace(base);
14+
</script>
15+
</head>
16+
<body>
17+
<p>Redirecting&hellip;</p>
18+
</body>
19+
</html>

src/components/InsightsSidebar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
PAGE_TYPES,
3333
type PageType,
3434
} from '../lib/report-schema';
35+
import { buildPathForPage } from '../lib/url-state';
3536
import type { ParsedReport, UsageReportRow } from '../lib/types';
3637
import { parseCSV } from '../lib/csv-parser';
3738
import { useReport } from '../context/useReport';
@@ -138,7 +139,7 @@ export function InsightsSidebar({
138139
return (
139140
<NavList.Item
140141
key={id}
141-
href="#"
142+
href={buildPathForPage(id)}
142143
aria-current={activePage === id && !activeProductFilter ? 'page' : undefined}
143144
defaultOpen={isUsagePage && activePage === PAGE_TYPES.USAGE}
144145
onClick={(event) => {
@@ -229,7 +230,7 @@ export function InsightsSidebar({
229230
{UTILITY_NAV_PAGES.map(({ id, label, icon: Icon }) => (
230231
<NavList.Item
231232
key={id}
232-
href="#"
233+
href={buildPathForPage(id)}
233234
aria-current={activePage === id ? 'page' : undefined}
234235
onClick={(event) => {
235236
event.preventDefault();

src/lib/url-state.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** Read/write filter state to URL search params */
1+
/** Read/write filter state to URL search params + path-based page routing */
22

33
export interface URLFilterState {
44
page?: string;
@@ -11,13 +11,38 @@ export interface URLFilterState {
1111
filters?: Record<string, string[]>;
1212
}
1313

14-
/** Parse filter state from current URL search params */
14+
const BASE_PATH = import.meta.env.BASE_URL || '/';
15+
const DEFAULT_PAGE = 'copilot';
16+
17+
/** Extract the page segment from the pathname (after the base path) */
18+
function getPageFromPath(): string | undefined {
19+
const path = window.location.pathname;
20+
const afterBase = path.startsWith(BASE_PATH)
21+
? path.slice(BASE_PATH.length)
22+
: path.slice(1);
23+
const segment = afterBase.replace(/\/$/, '');
24+
return segment || undefined;
25+
}
26+
27+
/** Build the pathname for a given page */
28+
export function buildPathForPage(page: string | undefined): string {
29+
if (!page || page === DEFAULT_PAGE) return BASE_PATH;
30+
return `${BASE_PATH}${page}`;
31+
}
32+
33+
/** Parse filter state from current URL path + search params */
1534
export function readURLFilterState(): URLFilterState {
1635
const params = new URLSearchParams(window.location.search);
1736
const state: URLFilterState = {};
1837

19-
const page = params.get('page');
20-
if (page) state.page = page;
38+
// Read page from path first, fall back to ?page= for backward compat
39+
const pathPage = getPageFromPath();
40+
const queryPage = params.get('page');
41+
if (pathPage) {
42+
state.page = pathPage;
43+
} else if (queryPage) {
44+
state.page = queryPage;
45+
}
2146

2247
const groupBy = params.get('groupBy');
2348
if (groupBy) state.groupBy = groupBy;
@@ -51,15 +76,16 @@ export function readURLFilterState(): URLFilterState {
5176
return state;
5277
}
5378

54-
/** Write filter state to URL search params without triggering navigation.
55-
* Merges with existing params so multiple callers don't clobber each other. */
79+
/** Write filter state to URL path + search params without triggering navigation.
80+
* Page goes in the path, everything else in query params. */
5681
export function writeURLFilterState(state: URLFilterState): void {
57-
// Start from current URL params to preserve values set by other callers
5882
const params = new URLSearchParams(window.location.search);
5983

60-
// Scalar params: set if provided, use defaults to omit default values
84+
// Remove legacy ?page= param if present
85+
params.delete('page');
86+
87+
// Scalar params (excluding page, which goes in the path)
6188
const SCALAR_DEFAULTS: Record<string, string> = {
62-
page: 'copilot',
6389
groupBy: 'username',
6490
timeBucket: 'daily',
6591
period: 'all',
@@ -101,7 +127,8 @@ export function writeURLFilterState(state: URLFilterState): void {
101127
}
102128

103129
const search = params.toString();
104-
const newURL = search ? `${window.location.pathname}?${search}` : window.location.pathname;
130+
const pagePath = buildPathForPage(state.page);
131+
const newURL = search ? `${pagePath}?${search}` : pagePath;
105132

106133
window.history.replaceState(null, '', newURL);
107134
}

0 commit comments

Comments
 (0)