Skip to content

Commit 8f5bc93

Browse files
authored
fix: correct data loading for GitHub Pages (#7)
- use relative paths for data files to work with base URL - improve error handling for fetch requests - add documentation for deployment to GitHub Pages - explain how to configure base path for static JSON and assets - add troubleshooting steps for common JSON parse error
1 parent fdea387 commit 8f5bc93

File tree

3 files changed

+49
-19
lines changed

3 files changed

+49
-19
lines changed

dashboard/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,20 @@ npm run dev # Start dev server
3232
npm run build # Production bundle
3333
npm run lint # ESLint
3434
```
35+
36+
## Deployment (GitHub Pages)
37+
When deploying to `https://<user>.github.io/<repo>/` you must build with the correct base path so that static JSON (in `public/data/`) and bundled assets resolve properly.
38+
39+
```bash
40+
# Example for this repository if the dashboard lives at /slcomp/
41+
export BASE_PATH=/slcomp/
42+
npm run build
43+
```
44+
45+
Then publish the contents of `dist/` to the `gh-pages` branch (or use an action). The data loader code uses `import.meta.env.BASE_URL` to construct paths like `<BASE_URL>data/database.json`, avoiding the common `Unexpected token '<'` JSON parse error that happens when a 404 HTML page is fetched instead of the JSON file.
46+
47+
If you see that error after deployment, confirm:
48+
1. The JSON files exist in `dist/data/` (they are copied from `public/data/`).
49+
2. `BASE_PATH` matched the repository subpath and ends with a trailing slash.
50+
3. Browser network panel requests resolve to `200` and not `404`/`301`.
51+

dashboard/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323
</head>
2424
<body>
2525
<div id="root"></div>
26-
<script type="module" src="/src/main.tsx"></script>
26+
<script type="module" src="src/main.tsx"></script>
2727
</body>
2828
</html>

dashboard/src/api.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
import type { DataRecord, ConsolidatedRecord, Dictionary, CutoutRecord } from './types';
22

3-
// In absence of a backend we will load static JSON via fetch (can be swapped later)
3+
// NOTE: When deployed on GitHub Pages the app is usually served from /<repo-name>/.
4+
// Using absolute paths (starting with "/") causes fetches to point at the domain root
5+
// (e.g. https://<user>.github.io/data/...) which 404s and returns the HTML index page.
6+
// That HTML then triggers: "SyntaxError: Unexpected token '<'" when res.json() is called.
7+
// We instead build URLs relative to Vite's injected BASE_URL (import.meta.env.BASE_URL).
48

5-
export const loadDatabase = async (): Promise<DataRecord[]> => {
6-
const res = await fetch('/data/database.json');
7-
return res.json();
8-
};
9+
const BASE_URL: string = (import.meta as { env?: Record<string, string> }).env?.BASE_URL || '/';
910

10-
export const loadConsolidated = async (): Promise<ConsolidatedRecord[]> => {
11-
const res = await fetch('/data/consolidated_database.json');
12-
return res.json();
11+
const buildDataUrl = (file: string) => {
12+
// Ensure exactly one trailing slash for BASE_URL then append data/<file>
13+
const base = BASE_URL.endsWith('/') ? BASE_URL : `${BASE_URL}/`;
14+
return `${base}data/${file}`;
1315
};
1416

15-
export const loadDictionary = async (): Promise<Dictionary> => {
16-
// dictionary.npy not easily consumable directly; expect a JSON conversion later.
17-
// Placeholder expects a json representation placed at /data/dictionary.json
18-
const res = await fetch('/data/dictionary.json');
19-
return res.json();
20-
};
17+
async function fetchJson<T>(file: string): Promise<T> {
18+
const url = buildDataUrl(file);
19+
const res = await fetch(url, { cache: 'no-cache' });
20+
if (!res.ok) {
21+
// Surface clearer diagnostics when something goes wrong (like path issues on Pages)
22+
const text = await res.text();
23+
throw new Error(`Failed to fetch ${url} (HTTP ${res.status}) - First 120 chars: ${text.slice(0, 120)}`);
24+
}
25+
try {
26+
return await res.json();
27+
} catch (err) {
28+
// Provide snippet of body to aid debugging of unexpected HTML responses
29+
const body = await res.clone().text().catch(() => '');
30+
throw new Error(`Invalid JSON at ${url}: ${(err as Error).message}. Snippet: ${body.slice(0, 120)}`);
31+
}
32+
}
2133

22-
export const loadCutouts = async (): Promise<CutoutRecord[]> => {
23-
const res = await fetch('/data/cutouts.json');
24-
return res.json();
25-
};
34+
// Public data loaders (can be swapped for real API later)
35+
export const loadDatabase = (): Promise<DataRecord[]> => fetchJson<DataRecord[]>('database.json');
36+
export const loadConsolidated = (): Promise<ConsolidatedRecord[]> => fetchJson<ConsolidatedRecord[]>('consolidated_database.json');
37+
export const loadDictionary = (): Promise<Dictionary> => fetchJson<Dictionary>('dictionary.json');
38+
export const loadCutouts = (): Promise<CutoutRecord[]> => fetchJson<CutoutRecord[]>('cutouts.json');
2639

2740
// MinIO direct image retrieval (signed URL pattern) - placeholder using fetch of gateway
2841
// Direct public path construction (no proxy). Expect objectKey like

0 commit comments

Comments
 (0)