diff --git a/README.md b/README.md
index de9c1cfe..0eb31072 100644
--- a/README.md
+++ b/README.md
@@ -1000,6 +1000,18 @@ export let loader: LoaderFunction = async ({ request }) => {
};
```
+#### XML
+
+Helper function to create a XML file response with any header.
+
+This is useful to create XML files based on data inside a Resource Route.
+
+```ts
+export let loader: LoaderFunction = async ({ request }) => {
+ return xml("");
+};
+```
+
### Typed Cookies
Cookie objects in Remix allows any type, the typed cookies from Remix Utils lets you use Zod to parse the cookie values and ensure they conform to a schema.
diff --git a/src/server/responses.ts b/src/server/responses.ts
index aa7b3647..aa3c2c7c 100644
--- a/src/server/responses.ts
+++ b/src/server/responses.ts
@@ -250,6 +250,34 @@ export function html(
});
}
+/**
+ * Create a response with a XML file response.
+ * It receives a string with the XML content and set the Content-Type header to
+ * `application/xml; charset=utf-8` always.
+ *
+ * This is useful to dynamically create a XML file from a Resource Route.
+ * @example
+ * export let loader: LoaderFunction = async ({ request }) => {
+ * return xml("");
+ * }
+ */
+export function xml(
+ content: string,
+ init: number | ResponseInit = {}
+): Response {
+ let responseInit = typeof init === "number" ? { status: init } : init;
+
+ let headers = new Headers(responseInit.headers);
+ if (!headers.has("Content-Type")) {
+ headers.set("Content-Type", "application/xml; charset=utf-8");
+ }
+
+ return new Response(content, {
+ ...responseInit,
+ headers,
+ });
+}
+
export type ImageType =
| "image/jpeg"
| "image/png"
diff --git a/test/server/responses.test.ts b/test/server/responses.test.ts
index 2b790b77..7713a609 100644
--- a/test/server/responses.test.ts
+++ b/test/server/responses.test.ts
@@ -2,6 +2,7 @@ import {
badRequest,
forbidden,
html,
+ xml,
image,
ImageType,
javascript,
@@ -295,6 +296,39 @@ describe("Responses", () => {
});
});
+ describe(xml, () => {
+ let content = "";
+ test("Should return Response with status 200", async () => {
+ let response = xml(content);
+ await expect(response.text()).resolves.toBe(content);
+ expect(response.status).toBe(200);
+ expect(response.headers.get("Content-Type")).toBe(
+ "application/xml; charset=utf-8"
+ );
+ });
+
+ test("Should allow defining the status as second options", async () => {
+ let response = xml(content, 201);
+ await expect(response.text()).resolves.toBe(content);
+ expect(response.status).toBe(201);
+ expect(response.headers.get("Content-Type")).toBe(
+ "application/xml; charset=utf-8"
+ );
+ });
+
+ test("Should allow changing the Response headers", async () => {
+ let response = xml(content, {
+ headers: { "X-Test": "it worked" },
+ });
+ await expect(response.text()).resolves.toBe(content);
+ expect(response.status).toBe(200);
+ expect(response.headers.get("Content-Type")).toBe(
+ "application/xml; charset=utf-8"
+ );
+ expect(response.headers.get("X-Test")).toBe("it worked");
+ });
+ });
+
describe(image, () => {
let content = new ArrayBuffer(0);
test.each([