From c198658b2ee33a19894bc2a1cef9ea2ee6fe2a42 Mon Sep 17 00:00:00 2001 From: Eduardo Cuducos <4732915+cuducos@users.noreply.github.com> Date: Wed, 13 May 2026 12:17:51 -0400 Subject: [PATCH] Allows relative path as site URL --- .gitignore | 1 + src/State.elm | 97 +++++++++++++++++++++++++++----------- src/Types.elm | 1 + src/build.js | 18 +++++-- {public => src}/index.html | 4 +- tests/StateTests.elm | 46 +++++++++++------- 6 files changed, 116 insertions(+), 51 deletions(-) rename {public => src}/index.html (95%) diff --git a/.gitignore b/.gitignore index 3d99bf6..c824991 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ public/app.js public/client-metadata.json public/crypto.js +public/index.html tests/elm-stuff/ diff --git a/src/State.elm b/src/State.elm index ff4f77e..75cd2a6 100644 --- a/src/State.elm +++ b/src/State.elm @@ -54,12 +54,16 @@ type Route = RouteHome | RouteNewPub | RoutePub String - | RouteCallback -parseRoute : Url.Url -> Route -parseRoute url = - case String.split "/" (trimSlashes url.path) of +parseRoute : String -> Url.Url -> Route +parseRoute basePath url = + let + stripped : String + stripped = + stripPrefix (trimSlashes basePath) (trimSlashes url.path) + in + case String.split "/" stripped of [ "" ] -> RouteHome @@ -73,45 +77,77 @@ parseRoute url = else RoutePub rkey - [ "callback" ] -> - RouteCallback - _ -> RouteHome +stripPrefix : String -> String -> String +stripPrefix prefix s = + if String.isEmpty prefix then + s + + else if String.startsWith (prefix ++ "/") s then + String.dropLeft (String.length prefix + 1) s + + else if s == prefix then + "" + + else + s + + trimSlashes : String -> String trimSlashes path = let - stripped : String - stripped = + withoutLeading : String + withoutLeading = if String.startsWith "/" path then String.dropLeft 1 path else path in - if String.endsWith "/" stripped then - String.dropRight 1 stripped + if String.endsWith "/" withoutLeading then + String.dropRight 1 withoutLeading else - stripped + withoutLeading + + +basePathFromAppUrl : String -> String +basePathFromAppUrl appUrl = + case Url.fromString appUrl of + Just u -> + if String.endsWith "/" u.path then + u.path + + else + u.path ++ "/" + + Nothing -> + "/" -routeToPath : Route -> String -routeToPath route = +routeToPath : String -> Route -> String +routeToPath basePath route = + let + prefix : String + prefix = + if String.endsWith "/" basePath then + String.dropRight 1 basePath + + else + basePath + in case route of RouteHome -> - "/" + prefix ++ "/" RouteNewPub -> - "/pub/new" + prefix ++ "/pub/new" RoutePub rkey -> - "/pub/" ++ rkey - - RouteCallback -> - "/callback" + prefix ++ "/pub/" ++ rkey @@ -189,10 +225,15 @@ actionFromId id = init : Flags -> Url.Url -> Nav.Key -> ( Model, Cmd Msg ) init flags url key = let + basePath : String + basePath = + basePathFromAppUrl flags.appUrl + config : Config config = { clientName = flags.clientName , appUrl = flags.appUrl + , basePath = basePath , plcDirectoryUrl = flags.plcDirectoryUrl , dohUrl = flags.dohUrl , bskyAppviewUrl = flags.bskyAppviewUrl @@ -204,7 +245,7 @@ init flags url key = redirectUri : String redirectUri = - flags.appUrl ++ "/callback" + flags.appUrl ++ "/" base : Model base = @@ -243,7 +284,7 @@ init flags url key = | page = Dashboard session , isLoading = True , pendingRequests = 2 - , selectedPubRkey = selectedFromRoute (parseRoute url) + , selectedPubRkey = selectedFromRoute (parseRoute basePath url) } , Cmd.batch [ signDpopFor ActionListPublications (xrpcSignInput session "listRecords") @@ -546,7 +587,7 @@ update msg model = RouteHome in ( { model | selectedPubRkey = maybeRkey } - , Nav.pushUrl model.navKey (routeToPath route) + , Nav.pushUrl model.navKey (routeToPath model.config.basePath route) ) InitPublication -> @@ -559,7 +600,7 @@ update msg model = emptyPublication :: model.publications , selectedPubRkey = Just "" } - , Nav.pushUrl model.navKey (routeToPath RouteNewPub) + , Nav.pushUrl model.navKey (routeToPath model.config.basePath RouteNewPub) ) UpdatePubName v -> @@ -669,7 +710,7 @@ update msg model = , oauthTokenEndpoint = "" , oauthParEndpoint = "" } - , Cmd.batch [ Ports.clearSession (), Nav.pushUrl model.navKey "/" ] + , Cmd.batch [ Ports.clearSession (), Nav.pushUrl model.navKey (routeToPath model.config.basePath RouteHome) ] ) LinkClicked urlRequest -> @@ -684,7 +725,7 @@ update msg model = let selected : Maybe String selected = - selectedFromRoute (parseRoute url) + selectedFromRoute (parseRoute model.config.basePath url) publications : List Publication publications = @@ -810,7 +851,7 @@ handleSuccess action body model = , Cmd.batch [ Ports.saveSession (sessionEncoder session) , Ports.clearPendingOAuth () - , Nav.pushUrl model.navKey "/" + , Nav.pushUrl model.navKey (routeToPath model.config.basePath RouteHome) , signDpopFor ActionListPublications (xrpcSignInput session "listRecords") , signDpopFor ActionListDocuments (xrpcSignInput session "listRecords") ] @@ -893,7 +934,7 @@ updateSavedRkey body model = Cmd.none else - Nav.replaceUrl model.navKey (routeToPath (RoutePub rkey)) + Nav.replaceUrl model.navKey (routeToPath model.config.basePath (RoutePub rkey)) in ( { model | publications = List.map apply model.publications diff --git a/src/Types.elm b/src/Types.elm index 2e6132b..c417b60 100644 --- a/src/Types.elm +++ b/src/Types.elm @@ -22,6 +22,7 @@ import Dict exposing (Dict) type alias Config = { clientName : String , appUrl : String + , basePath : String , plcDirectoryUrl : String , dohUrl : String , bskyAppviewUrl : String diff --git a/src/build.js b/src/build.js index de6d549..35a851e 100644 --- a/src/build.js +++ b/src/build.js @@ -15,11 +15,16 @@ const BSKY_APPVIEW_URL = process.env.BSKY_APPVIEW_URL || "https://api.bsky.app"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const basePath = (() => { + const path = new URL(APP_URL).pathname; + return path.endsWith("/") ? path : path + "/"; +})(); + const metadata = { client_id: `${APP_URL}/client-metadata.json`, client_name: APP_NAME, client_uri: APP_URL, - redirect_uris: [`${APP_URL}/callback`], + redirect_uris: [APP_URL + "/"], scope: "atproto", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], @@ -41,10 +46,13 @@ const substitutions = { __BSKY_APPVIEW_URL__: BSKY_APPVIEW_URL, }; -const source = readFileSync(join(root, "src", "crypto.js"), "utf8"); -const output = Object.entries(substitutions).reduce( +const cryptoSource = readFileSync(join(root, "src", "crypto.js"), "utf8"); +const cryptoOutput = Object.entries(substitutions).reduce( (acc, [token, value]) => acc.replaceAll(token, JSON.stringify(value)), - source, + cryptoSource, ); +writeFileSync(join(root, "public", "crypto.js"), cryptoOutput); -writeFileSync(join(root, "public", "crypto.js"), output); +const htmlSource = readFileSync(join(root, "src", "index.html"), "utf8"); +const htmlOutput = htmlSource.replaceAll("__BASE__", basePath); +writeFileSync(join(root, "public", "index.html"), htmlOutput); diff --git a/public/index.html b/src/index.html similarity index 95% rename from public/index.html rename to src/index.html index 6134ce3..3248c10 100644 --- a/public/index.html +++ b/src/index.html @@ -95,7 +95,7 @@
- - + + diff --git a/tests/StateTests.elm b/tests/StateTests.elm index 9d403a0..de7a90e 100644 --- a/tests/StateTests.elm +++ b/tests/StateTests.elm @@ -193,25 +193,39 @@ suite = Err err -> Expect.fail err ] - , describe "parseRoute" + , describe "parseRoute (no base path)" [ test "parses /" <| - \_ -> Expect.equal RouteHome (routeFor "/") + \_ -> Expect.equal RouteHome (routeFor "/" "/") , test "parses /pub/new" <| - \_ -> Expect.equal RouteNewPub (routeFor "/pub/new") + \_ -> Expect.equal RouteNewPub (routeFor "/" "/pub/new") , test "parses /pub/" <| - \_ -> Expect.equal (RoutePub "abc123") (routeFor "/pub/abc123") - , test "parses /callback" <| - \_ -> Expect.equal RouteCallback (routeFor "/callback") + \_ -> Expect.equal (RoutePub "abc123") (routeFor "/" "/pub/abc123") , test "unknown path falls back to home" <| - \_ -> Expect.equal RouteHome (routeFor "/anything/else") + \_ -> Expect.equal RouteHome (routeFor "/" "/anything/else") + ] + , describe "parseRoute (with base path /std-pub/)" + [ test "parses /std-pub/" <| + \_ -> Expect.equal RouteHome (routeFor "/std-pub/" "/std-pub/") + , test "parses /std-pub (no trailing slash)" <| + \_ -> Expect.equal RouteHome (routeFor "/std-pub/" "/std-pub") + , test "parses /std-pub/pub/abc123" <| + \_ -> Expect.equal (RoutePub "abc123") (routeFor "/std-pub/" "/std-pub/pub/abc123") + , test "parses /std-pub/pub/new" <| + \_ -> Expect.equal RouteNewPub (routeFor "/std-pub/" "/std-pub/pub/new") ] , describe "routeToPath" - [ test "home is /" <| - \_ -> Expect.equal "/" (routeToPath RouteHome) - , test "publication has rkey" <| - \_ -> Expect.equal "/pub/xyz" (routeToPath (RoutePub "xyz")) - , test "new is /pub/new" <| - \_ -> Expect.equal "/pub/new" (routeToPath RouteNewPub) + [ test "home at root base is /" <| + \_ -> Expect.equal "/" (routeToPath "/" RouteHome) + , test "publication at root base" <| + \_ -> Expect.equal "/pub/xyz" (routeToPath "/" (RoutePub "xyz")) + , test "new at root base" <| + \_ -> Expect.equal "/pub/new" (routeToPath "/" RouteNewPub) + , test "home at /std-pub/ base" <| + \_ -> Expect.equal "/std-pub/" (routeToPath "/std-pub/" RouteHome) + , test "publication at /std-pub/ base" <| + \_ -> Expect.equal "/std-pub/pub/xyz" (routeToPath "/std-pub/" (RoutePub "xyz")) + , test "new at /std-pub/ base" <| + \_ -> Expect.equal "/std-pub/pub/new" (routeToPath "/std-pub/" RouteNewPub) ] , describe "documentUrl" [ test "uses canonical url when present" <| @@ -372,11 +386,11 @@ suite = ] -routeFor : String -> Route -routeFor path = +routeFor : String -> String -> Route +routeFor basePath path = case Url.fromString ("http://app.test" ++ path) of Just u -> - parseRoute u + parseRoute basePath u Nothing -> RouteHome -- 2.51.2