Skip to content

Commit af597fe

Browse files
committed
fix(demo): prerender deep routes + derive SMART URLs from origin
The static demo on GitHub Pages was unusable for two reasons: 1. Only the root '/' was prerendered. Direct hits to /demo/patients or /demo/playground fell through to the site-level 404.html (which is Docusaurus's, not our SPA shell), so deep links 404'd. Fix: enable crawlLinks so the prerender follows the navbar links and emits patients/index.html and playground/index.html. 2. The OAuth redirect_uri and EHR-launch URL were hardcoded to http://localhost:3000. Even after login the callback would try to hit a localhost dev server. Fix: derive them at runtime from window.location.origin + import.meta.env.BASE_URL via getRedirectUri() / getAppLaunchUrl() helpers. Works for localhost dev AND for /fhir-dsl/demo without any env-var juggling. Env-var overrides still honored. Also stop overwriting the prerendered index.html in the workflow's stage step — only 404.html needs the SPA-shell fallback now.
1 parent 01e84ab commit af597fe

5 files changed

Lines changed: 43 additions & 13 deletions

File tree

.github/workflows/deploy-docs.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,11 @@ jobs:
7070
DEMO_DST=apps/docs/build/demo
7171
mkdir -p "$DEMO_DST"
7272
cp -R "$DEMO_SRC"/. "$DEMO_DST"/
73-
# GitHub Pages serves /demo/ → /demo/index.html (root-route SPA shell);
74-
# /demo/<deep-route> → /demo/404.html (which is the same SPA shell, so the
75-
# client-side router handles the deep route after hydration).
76-
cp "$DEMO_DST/_shell.html" "$DEMO_DST/index.html"
73+
# Prerender already produced index.html for /, /patients, /playground.
74+
# Copy _shell.html → 404.html so direct hits to unrendered routes
75+
# (e.g. /patients/<id>) still load the SPA shell and let the client
76+
# router resolve the deep link. (GitHub Pages's site-level 404
77+
# fallback is Docusaurus's, not ours, so per-app 404.html is needed.)
7778
cp "$DEMO_DST/_shell.html" "$DEMO_DST/404.html"
7879
echo "demo size: $(du -sh $DEMO_DST | cut -f1)"
7980

apps/his-demo/src/lib/smart-config.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,46 @@
99
* `/launch` route with `?iss=…&launch=…` populated, and our existing /launch
1010
* handler runs the full PKCE dance.
1111
*/
12+
// Resolve the public origin the SPA is served from, including any subpath
13+
// (e.g. /fhir-dsl/demo/). Used to derive redirect URIs at runtime so the
14+
// SMART flow works on localhost dev AND on https://awbx.github.io/fhir-dsl/demo
15+
// without env-var juggling. Only call from client-side code (uses `window`).
16+
function publicAppUrl(routePath: string): string {
17+
const base = import.meta.env.BASE_URL ?? "/";
18+
const path = routePath.startsWith("/") ? routePath.slice(1) : routePath;
19+
return `${window.location.origin}${base}${path}`;
20+
}
21+
1222
export const SMART_CONFIG = {
1323
/** Smart Health IT's launcher app — entry point for the demo's EHR-launch flow. */
1424
launcherUrl:
1525
import.meta.env.VITE_SMART_LAUNCHER_URL ?? "https://launch.smarthealthit.org",
1626
/** FHIR version the launcher should simulate (must match the sandbox endpoint). */
1727
launcherFhirVersion: import.meta.env.VITE_SMART_LAUNCHER_FHIR_VERSION ?? "r4",
1828
clientId: import.meta.env.VITE_SMART_CLIENT_ID ?? "fhir-dsl-demo",
19-
redirectUri: import.meta.env.VITE_SMART_REDIRECT_URI ?? "http://localhost:3000/callback",
20-
/** EHR-launch entry point for our app — where the launcher hands us back. */
21-
appLaunchUrl: import.meta.env.VITE_SMART_APP_LAUNCH_URL ?? "http://localhost:3000/launch",
2229
scope:
2330
import.meta.env.VITE_SMART_SCOPE ??
2431
"launch openid fhirUser patient/*.read offline_access",
2532
/** Default upstream FHIR for queries after login (R4). */
2633
fhirBaseUrl: import.meta.env.VITE_FHIR_BASE_URL ?? "https://hapi.fhir.org/baseR4",
2734
} as const;
2835

36+
/** OAuth redirect_uri — derived at call-site so it tracks the deployment URL. */
37+
export function getRedirectUri(): string {
38+
return import.meta.env.VITE_SMART_REDIRECT_URI ?? publicAppUrl("callback");
39+
}
40+
41+
/** EHR-launch entry point — where the launcher hands us back. */
42+
export function getAppLaunchUrl(): string {
43+
return import.meta.env.VITE_SMART_APP_LAUNCH_URL ?? publicAppUrl("launch");
44+
}
45+
2946
export const TOKEN_STORE_KEY = `user:${SMART_CONFIG.clientId}`;
3047

3148
/** Build the URL the "Login with SMART" button should point at. */
3249
export function buildLauncherEntryUrl(): string {
3350
const params = new URLSearchParams({
34-
launch_url: SMART_CONFIG.appLaunchUrl,
51+
launch_url: getAppLaunchUrl(),
3552
fhir_version: SMART_CONFIG.launcherFhirVersion,
3653
});
3754
return `${SMART_CONFIG.launcherUrl}/?${params.toString()}`;

apps/his-demo/src/routes/callback.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
22
import { useEffect, useState } from "react";
33
import { exchangeCode, type SmartConfiguration } from "@fhir-dsl/smart";
44
import { withAbsoluteExpiry } from "@fhir-dsl/smart";
5-
import { SMART_CONFIG, TOKEN_STORE_KEY, buildLauncherEntryUrl } from "#/lib/smart-config";
5+
import {
6+
SMART_CONFIG,
7+
TOKEN_STORE_KEY,
8+
buildLauncherEntryUrl,
9+
getRedirectUri,
10+
} from "#/lib/smart-config";
611
import { tokenStore } from "#/lib/smart-store";
712
import { formatError } from "#/lib/error-toast";
813

@@ -64,7 +69,7 @@ function CallbackPage() {
6469
const tokens = await exchangeCode({
6570
smartConfig: pending.smartConfig,
6671
clientId: SMART_CONFIG.clientId,
67-
redirectUri: SMART_CONFIG.redirectUri,
72+
redirectUri: getRedirectUri(),
6873
code,
6974
codeVerifier: pending.codeVerifier,
7075
});

apps/his-demo/src/routes/launch.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
generateCodeVerifier,
88
generateState,
99
} from "@fhir-dsl/smart";
10-
import { SMART_CONFIG, buildLauncherEntryUrl } from "#/lib/smart-config";
10+
import { SMART_CONFIG, buildLauncherEntryUrl, getRedirectUri } from "#/lib/smart-config";
1111
import { formatError } from "#/lib/error-toast";
1212

1313
interface LaunchSearch {
@@ -56,7 +56,7 @@ function LaunchPage() {
5656
const authorizeUrl = buildAuthorizeUrl({
5757
smartConfig,
5858
clientId: SMART_CONFIG.clientId,
59-
redirectUri: SMART_CONFIG.redirectUri,
59+
redirectUri: getRedirectUri(),
6060
scope: SMART_CONFIG.scope,
6161
state,
6262
codeChallenge,

apps/his-demo/vite.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ const config = defineConfig({
2222
tanstackStart({
2323
spa: {
2424
enabled: true,
25-
prerender: { enabled: true },
25+
// crawlLinks follows <a> tags from the SPA shell so the navbar's
26+
// /patients and /playground get prerendered as physical
27+
// dist/client/<route>/index.html files. Required because GitHub
28+
// Pages's 404 fallback is site-level (/fhir-dsl/404.html =
29+
// Docusaurus's 404), not per-subdir, so deep links need real
30+
// files instead of a SPA-shell fallback. Parameterized routes
31+
// (/patients/$id) still only resolve via in-app navigation.
32+
prerender: { enabled: true, crawlLinks: true },
2633
},
2734
}),
2835
viteReact(),

0 commit comments

Comments
 (0)