Skip to content

Commit d91b97a

Browse files
Add openimg for image optimization (#935)
Co-authored-by: Kent C. Dodds <[email protected]> Co-authored-by: Kent C. Dodds <[email protected]>
1 parent a5cbab6 commit d91b97a

23 files changed

+835
-191
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ node_modules
1515
/playwright/.cache/
1616
/tests/fixtures/email/
1717
/tests/fixtures/uploaded/
18+
/tests/fixtures/openimg/
1819
/coverage
1920

2021
/other/cache.db

app/components/user-dropdown.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Img } from 'openimg/react'
12
import { useRef } from 'react'
23
import { Link, Form } from 'react-router'
34
import { getUserImgSrc } from '#app/utils/misc.tsx'
@@ -25,10 +26,12 @@ export function UserDropdown() {
2526
onClick={(e) => e.preventDefault()}
2627
className="flex items-center gap-2"
2728
>
28-
<img
29+
<Img
2930
className="h-8 w-8 rounded-full object-cover"
3031
alt={user.name ?? user.username}
31-
src={getUserImgSrc(user.image?.id)}
32+
src={getUserImgSrc(user.image?.objectKey)}
33+
width={256}
34+
height={256}
3235
/>
3336
<span className="text-body-sm font-bold">
3437
{user.name ?? user.username}

app/root.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { OpenImgContextProvider } from 'openimg/react'
12
import {
23
data,
34
Link,
@@ -33,7 +34,7 @@ import { prisma } from './utils/db.server.ts'
3334
import { getEnv } from './utils/env.server.ts'
3435
import { pipeHeaders } from './utils/headers.server.ts'
3536
import { honeypot } from './utils/honeypot.server.ts'
36-
import { combineHeaders, getDomainUrl } from './utils/misc.tsx'
37+
import { combineHeaders, getDomainUrl, getImgSrc } from './utils/misc.tsx'
3738
import { useNonce } from './utils/nonce-provider.ts'
3839
import { type Theme, getTheme } from './utils/theme.server.ts'
3940
import { makeTimings, time } from './utils/timing.server.ts'
@@ -83,7 +84,7 @@ export async function loader({ request }: Route.LoaderArgs) {
8384
id: true,
8485
name: true,
8586
username: true,
86-
image: { select: { id: true } },
87+
image: { select: { objectKey: true } },
8788
roles: {
8889
select: {
8990
name: true,
@@ -194,7 +195,10 @@ function App() {
194195
useToast(data.toast)
195196

196197
return (
197-
<>
198+
<OpenImgContextProvider
199+
optimizerEndpoint="/resources/images"
200+
getSrc={getImgSrc}
201+
>
198202
<div className="flex min-h-screen flex-col justify-between">
199203
<header className="container py-6">
200204
<nav className="flex flex-wrap items-center justify-between gap-4 sm:flex-nowrap md:gap-8">
@@ -226,7 +230,7 @@ function App() {
226230
</div>
227231
<EpicToaster closeButton position="top-center" theme={theme} />
228232
<EpicProgress />
229-
</>
233+
</OpenImgContextProvider>
230234
)
231235
}
232236

app/routes/resources+/download-user-data.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { requireUserId } from '#app/utils/auth.server.ts'
22
import { prisma } from '#app/utils/db.server.ts'
3-
import { getDomainUrl } from '#app/utils/misc.tsx'
3+
import { getDomainUrl, getNoteImgSrc, getUserImgSrc } from '#app/utils/misc.tsx'
44
import { type Route } from './+types/download-user-data.ts'
55

66
export async function loader({ request }: Route.LoaderArgs) {
@@ -47,14 +47,14 @@ export async function loader({ request }: Route.LoaderArgs) {
4747
image: user.image
4848
? {
4949
...user.image,
50-
url: `${domain}/resources/user-images/${user.image.id}`,
50+
url: domain + getUserImgSrc(user.image.objectKey),
5151
}
5252
: null,
5353
notes: user.notes.map((note) => ({
5454
...note,
5555
images: note.images.map((image) => ({
5656
...image,
57-
url: `${domain}/resources/note-images/${image.id}`,
57+
url: domain + getNoteImgSrc(image.objectKey),
5858
})),
5959
})),
6060
},

app/routes/resources+/images.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { invariantResponse } from '@epic-web/invariant'
2+
import { getImgResponse } from 'openimg/node'
3+
import { getDomainUrl } from '#app/utils/misc.tsx'
4+
import { getSignedGetRequestInfo } from '#app/utils/storage.server.ts'
5+
import { type Route } from './+types/images'
6+
7+
export async function loader({ request }: Route.LoaderArgs) {
8+
const url = new URL(request.url)
9+
const searchParams = url.searchParams
10+
11+
const headers = new Headers()
12+
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
13+
14+
const objectKey = searchParams.get('objectKey')
15+
16+
return getImgResponse(request, {
17+
headers,
18+
allowlistedOrigins: [
19+
getDomainUrl(request),
20+
process.env.AWS_ENDPOINT_URL_S3,
21+
].filter(Boolean),
22+
cacheFolder:
23+
process.env.NODE_ENV === 'production'
24+
? '/data/images'
25+
: './tests/fixtures/openimg',
26+
getImgSource: () => {
27+
if (objectKey) {
28+
const { url: signedUrl, headers: signedHeaders } =
29+
getSignedGetRequestInfo(objectKey)
30+
return {
31+
type: 'fetch',
32+
url: signedUrl,
33+
headers: signedHeaders,
34+
}
35+
}
36+
37+
const src = searchParams.get('src')
38+
invariantResponse(src, 'src query parameter is required', { status: 400 })
39+
40+
if (URL.canParse(src)) {
41+
// Fetch image from external URL; will be matched against allowlist
42+
return {
43+
type: 'fetch',
44+
url: src,
45+
}
46+
}
47+
// Retrieve image from filesystem (public folder)
48+
if (src.startsWith('/assets')) {
49+
// Files managed by Vite
50+
return {
51+
type: 'fs',
52+
path: '.' + src,
53+
}
54+
}
55+
// Fallback to files in public folder
56+
return {
57+
type: 'fs',
58+
path: './public' + src,
59+
}
60+
},
61+
})
62+
}

app/routes/resources+/note-images.$imageId.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

app/routes/resources+/user-images.$imageId.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

app/routes/settings+/profile.index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react'
22
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
33
import { invariantResponse } from '@epic-web/invariant'
44
import { type SEOHandle } from '@nasa-gcn/remix-seo'
5+
import { Img } from 'openimg/react'
56
import { data, Link, useFetcher } from 'react-router'
67
import { z } from 'zod'
78
import { ErrorList, Field } from '#app/components/forms.tsx'
@@ -36,7 +37,7 @@ export async function loader({ request }: Route.LoaderArgs) {
3637
username: true,
3738
email: true,
3839
image: {
39-
select: { id: true },
40+
select: { objectKey: true },
4041
},
4142
_count: {
4243
select: {
@@ -101,10 +102,13 @@ export default function EditUserProfile({ loaderData }: Route.ComponentProps) {
101102
<div className="flex flex-col gap-12">
102103
<div className="flex justify-center">
103104
<div className="relative h-52 w-52">
104-
<img
105-
src={getUserImgSrc(loaderData.user.image?.id)}
105+
<Img
106+
src={getUserImgSrc(loaderData.user.image?.objectKey)}
106107
alt={loaderData.user.username}
107108
className="h-full w-full rounded-full object-cover"
109+
width={832}
110+
height={832}
111+
isAboveFold
108112
/>
109113
<Button
110114
asChild

app/routes/settings+/profile.photo.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function loader({ request }: Route.LoaderArgs) {
5656
id: true,
5757
name: true,
5858
username: true,
59-
image: { select: { id: true } },
59+
image: { select: { objectKey: true } },
6060
},
6161
})
6262
invariantResponse(user, 'User not found', { status: 404 })
@@ -142,7 +142,9 @@ export default function PhotoRoute({
142142
<img
143143
src={
144144
newImageSrc ??
145-
(loaderData.user ? getUserImgSrc(loaderData.user.image?.id) : '')
145+
(loaderData.user
146+
? getUserImgSrc(loaderData.user.image?.objectKey)
147+
: '')
146148
}
147149
className="h-52 w-52 rounded-full object-cover"
148150
alt={loaderData.user?.name ?? loaderData.user?.username}
@@ -202,7 +204,7 @@ export default function PhotoRoute({
202204
>
203205
<Icon name="trash">Reset</Icon>
204206
</Button>
205-
{loaderData.user.image?.id ? (
207+
{loaderData.user.image ? (
206208
<StatusButton
207209
className="peer-valid:hidden"
208210
variant="destructive"

app/routes/users+/$username.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { invariantResponse } from '@epic-web/invariant'
2+
import { Img } from 'openimg/react'
23
import {
34
type LoaderFunctionArgs,
45
Form,
@@ -21,7 +22,7 @@ export async function loader({ params }: LoaderFunctionArgs) {
2122
name: true,
2223
username: true,
2324
createdAt: true,
24-
image: { select: { id: true } },
25+
image: { select: { id: true, objectKey: true } },
2526
},
2627
where: {
2728
username: params.username,
@@ -48,10 +49,12 @@ export default function ProfileRoute() {
4849
<div className="relative w-52">
4950
<div className="absolute -top-40">
5051
<div className="relative">
51-
<img
52-
src={getUserImgSrc(data.user.image?.id)}
52+
<Img
53+
src={getUserImgSrc(data.user.image?.objectKey)}
5354
alt={userDisplayName}
5455
className="h-52 w-52 rounded-full object-cover"
56+
width={832}
57+
height={832}
5558
/>
5659
</div>
5760
</div>

0 commit comments

Comments
 (0)