Skip to content

Commit ab995e3

Browse files
committed
feat: Add Resend with Remix example
0 parents  commit ab995e3

15 files changed

+7799
-0
lines changed

Diff for: .env.example

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
RESEND_API_KEY=""

Diff for: .eslintrc.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import('eslint').Linter.Config} */
2+
module.exports = {
3+
extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4+
};

Diff for: .gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
/public/build
6+
.env

Diff for: README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Resend with Remix
2+
3+
This example shows how to use Resend with [Remix](https://remix.run).
4+
5+
## Instructions
6+
7+
1. Define environment variables in `.env` file.
8+
9+
2. Install dependencies:
10+
11+
```sh
12+
npm install
13+
# or
14+
yarn
15+
```
16+
17+
3. Run Remix locally:
18+
19+
```sh
20+
npm run dev
21+
```
22+
23+
4. Open URL in the browser:
24+
25+
```
26+
http://localhost:3000/send
27+
```

Diff for: app/entry.client.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* By default, Remix will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.client
5+
*/
6+
7+
import { RemixBrowser } from "@remix-run/react";
8+
import { startTransition, StrictMode } from "react";
9+
import { hydrateRoot } from "react-dom/client";
10+
11+
startTransition(() => {
12+
hydrateRoot(
13+
document,
14+
<StrictMode>
15+
<RemixBrowser />
16+
</StrictMode>
17+
);
18+
});

Diff for: app/entry.server.tsx

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* By default, Remix will handle generating the HTTP Response for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.server
5+
*/
6+
7+
import { PassThrough } from "node:stream";
8+
9+
import type { AppLoadContext, EntryContext } from "@remix-run/node";
10+
import { Response } from "@remix-run/node";
11+
import { RemixServer } from "@remix-run/react";
12+
import isbot from "isbot";
13+
import { renderToPipeableStream } from "react-dom/server";
14+
15+
const ABORT_DELAY = 5_000;
16+
17+
export default function handleRequest(
18+
request: Request,
19+
responseStatusCode: number,
20+
responseHeaders: Headers,
21+
remixContext: EntryContext,
22+
loadContext: AppLoadContext
23+
) {
24+
return isbot(request.headers.get("user-agent"))
25+
? handleBotRequest(
26+
request,
27+
responseStatusCode,
28+
responseHeaders,
29+
remixContext
30+
)
31+
: handleBrowserRequest(
32+
request,
33+
responseStatusCode,
34+
responseHeaders,
35+
remixContext
36+
);
37+
}
38+
39+
function handleBotRequest(
40+
request: Request,
41+
responseStatusCode: number,
42+
responseHeaders: Headers,
43+
remixContext: EntryContext
44+
) {
45+
return new Promise((resolve, reject) => {
46+
const { pipe, abort } = renderToPipeableStream(
47+
<RemixServer
48+
context={remixContext}
49+
url={request.url}
50+
abortDelay={ABORT_DELAY}
51+
/>,
52+
{
53+
onAllReady() {
54+
const body = new PassThrough();
55+
56+
responseHeaders.set("Content-Type", "text/html");
57+
58+
resolve(
59+
new Response(body, {
60+
headers: responseHeaders,
61+
status: responseStatusCode,
62+
})
63+
);
64+
65+
pipe(body);
66+
},
67+
onShellError(error: unknown) {
68+
reject(error);
69+
},
70+
onError(error: unknown) {
71+
responseStatusCode = 500;
72+
console.error(error);
73+
},
74+
}
75+
);
76+
77+
setTimeout(abort, ABORT_DELAY);
78+
});
79+
}
80+
81+
function handleBrowserRequest(
82+
request: Request,
83+
responseStatusCode: number,
84+
responseHeaders: Headers,
85+
remixContext: EntryContext
86+
) {
87+
return new Promise((resolve, reject) => {
88+
const { pipe, abort } = renderToPipeableStream(
89+
<RemixServer
90+
context={remixContext}
91+
url={request.url}
92+
abortDelay={ABORT_DELAY}
93+
/>,
94+
{
95+
onShellReady() {
96+
const body = new PassThrough();
97+
98+
responseHeaders.set("Content-Type", "text/html");
99+
100+
resolve(
101+
new Response(body, {
102+
headers: responseHeaders,
103+
status: responseStatusCode,
104+
})
105+
);
106+
107+
pipe(body);
108+
},
109+
onShellError(error: unknown) {
110+
reject(error);
111+
},
112+
onError(error: unknown) {
113+
console.error(error);
114+
responseStatusCode = 500;
115+
},
116+
}
117+
);
118+
119+
setTimeout(abort, ABORT_DELAY);
120+
});
121+
}

Diff for: app/root.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { cssBundleHref } from "@remix-run/css-bundle";
2+
import type { LinksFunction } from "@remix-run/node";
3+
import {
4+
Links,
5+
LiveReload,
6+
Meta,
7+
Outlet,
8+
Scripts,
9+
ScrollRestoration,
10+
} from "@remix-run/react";
11+
12+
export const links: LinksFunction = () => [
13+
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
14+
];
15+
16+
export default function App() {
17+
return (
18+
<html lang="en">
19+
<head>
20+
<meta charSet="utf-8" />
21+
<meta name="viewport" content="width=device-width,initial-scale=1" />
22+
<Meta />
23+
<Links />
24+
</head>
25+
<body>
26+
<Outlet />
27+
<ScrollRestoration />
28+
<Scripts />
29+
<LiveReload />
30+
</body>
31+
</html>
32+
);
33+
}

Diff for: app/routes/_index.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { V2_MetaFunction } from "@remix-run/node";
2+
3+
export const meta: V2_MetaFunction = () => {
4+
return [
5+
{ title: "Resend + Remix" },
6+
{ name: "description", content: "An example of Remix with Resend" },
7+
];
8+
};
9+
10+
export default function Index() {
11+
return (
12+
<h1>Welcome to Resend + Remix example</h1>
13+
);
14+
}

Diff for: app/routes/send.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { json } from '@remix-run/node';
2+
import { Resend } from 'resend';
3+
4+
const resend = new Resend(process.env.RESEND_API_KEY);
5+
6+
export const loader = async () => {
7+
try {
8+
const data = await resend.sendEmail({
9+
10+
11+
subject: 'Hello world',
12+
html: '<strong>It works!</strong>',
13+
});
14+
15+
return json(data, 200);
16+
}
17+
catch (error) {
18+
return json({ error }, 400);
19+
}
20+
};

Diff for: package.json

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"private": true,
3+
"sideEffects": false,
4+
"scripts": {
5+
"build": "remix build",
6+
"dev": "remix dev",
7+
"start": "remix-serve build",
8+
"typecheck": "tsc"
9+
},
10+
"dependencies": {
11+
"@remix-run/css-bundle": "^1.16.1",
12+
"@remix-run/node": "^1.16.1",
13+
"@remix-run/react": "^1.16.1",
14+
"@remix-run/serve": "^1.16.1",
15+
"isbot": "^3.6.8",
16+
"react": "^18.2.0",
17+
"react-dom": "^18.2.0",
18+
"resend": "^0.15.1"
19+
},
20+
"devDependencies": {
21+
"@remix-run/dev": "^1.16.1",
22+
"@remix-run/eslint-config": "^1.16.1",
23+
"@types/react": "^18.0.35",
24+
"@types/react-dom": "^18.0.11",
25+
"eslint": "^8.38.0",
26+
"typescript": "^5.0.4"
27+
},
28+
"engines": {
29+
"node": ">=14"
30+
}
31+
}

Diff for: public/favicon.ico

16.6 KB
Binary file not shown.

Diff for: remix.config.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/** @type {import('@remix-run/dev').AppConfig} */
2+
module.exports = {
3+
ignoredRouteFiles: ["**/.*"],
4+
// appDirectory: "app",
5+
// assetsBuildDirectory: "public/build",
6+
// serverBuildPath: "build/index.js",
7+
// publicPath: "/build/",
8+
serverModuleFormat: "cjs",
9+
future: {
10+
v2_errorBoundary: true,
11+
v2_meta: true,
12+
v2_normalizeFormMethod: true,
13+
v2_routeConvention: true,
14+
},
15+
};

Diff for: remix.env.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// <reference types="@remix-run/dev" />
2+
/// <reference types="@remix-run/node" />

Diff for: tsconfig.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3+
"compilerOptions": {
4+
"lib": ["DOM", "DOM.Iterable", "ES2019"],
5+
"isolatedModules": true,
6+
"esModuleInterop": true,
7+
"jsx": "react-jsx",
8+
"moduleResolution": "node",
9+
"resolveJsonModule": true,
10+
"target": "ES2019",
11+
"strict": true,
12+
"allowJs": true,
13+
"forceConsistentCasingInFileNames": true,
14+
"baseUrl": ".",
15+
"paths": {
16+
"~/*": ["./app/*"]
17+
},
18+
19+
// Remix takes care of building everything in `remix build`.
20+
"noEmit": true
21+
}
22+
}

0 commit comments

Comments
 (0)