diff --git a/contributors.yml b/contributors.yml index e28ff438df..759fd10a60 100644 --- a/contributors.yml +++ b/contributors.yml @@ -253,6 +253,7 @@ - KimHyeongRae0 - kirillgroshkov - kkirsche +- kklem0 - kno-raziel - knownasilya - koojaa diff --git a/integration/react-router-serve-test.ts b/integration/react-router-serve-test.ts index 361de2d1ef..1d0bd13132 100644 --- a/integration/react-router-serve-test.ts +++ b/integration/react-router-serve-test.ts @@ -81,4 +81,46 @@ test.describe("react-router-serve", () => { }); }); } + + test.describe("well-known URIs", () => { + for (const templateName of templateNames) { + test.describe(`template: ${templateName}`, () => { + let fixture: Fixture; + let appFixture: AppFixture; + + test.beforeAll(async () => { + fixture = await createFixture({ + templateName, + useReactRouterServe: true, + files: { + "public/.well-known/security.txt": + "Contact: mailto:security@example.com", + "public/.hidden.txt": "should not be served", + }, + }); + appFixture = await createAppFixture(fixture); + }); + + test.afterAll(() => { + appFixture.close(); + }); + + test("serves files under /.well-known", async () => { + let response = await fetch( + `${appFixture.serverUrl}/.well-known/security.txt`, + ); + expect(response.status).toBe(200); + expect(await response.text()).toContain( + "Contact: mailto:security@example.com", + ); + }); + + test("keeps other dotfiles hidden", async () => { + let response = await fetch(`${appFixture.serverUrl}/.hidden.txt`); + expect(response.status).toBe(404); + expect(await response.text()).not.toContain("should not be served"); + }); + }); + } + }); }); diff --git a/packages/react-router-serve/.changes/patch.serve-well-known.md b/packages/react-router-serve/.changes/patch.serve-well-known.md new file mode 100644 index 0000000000..5ce749ce83 --- /dev/null +++ b/packages/react-router-serve/.changes/patch.serve-well-known.md @@ -0,0 +1 @@ +Serve `/.well-known/*` files from the client build directory. Express 5's static middleware ignores every dot-segment path by default, so RFC 8615 well-known URIs — ACME challenges, Android's `assetlinks.json`, Apple's `apple-app-site-association` — fell through to the request handler and came back as app-rendered HTML instead of the static file. Other dotfiles remain hidden. diff --git a/packages/react-router-serve/cli.ts b/packages/react-router-serve/cli.ts index b2bc504889..5bb8e22575 100644 --- a/packages/react-router-serve/cli.ts +++ b/packages/react-router-serve/cli.ts @@ -206,6 +206,10 @@ async function run() { ); app.use(expressPublicPath, express.static(build.assetsBuildDirectory)); app.use(express.static("public", { maxAge: "1h" })); + app.use( + "/.well-known", + express.static(path.join(build.assetsBuildDirectory, ".well-known")), + ); app.use(morgan("tiny")); if (build.fetch) {