diff --git a/.eslintrc.js b/.eslintrc.js index 7ab79eed..4b5e94d2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -214,6 +214,12 @@ module.exports = { // @see {@link https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318} 'react/no-array-index-key': 'warn', + // Allow `tw` properties when using Tailwind with `@vercel/og`. + // @see {@link https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md} + // @see {@link https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation} + // @see {@link http://bit.ly/3Kfwovz} + 'react/no-unknown-property': ['error', { ignore: ['tw'] }], + // Configure `jsx-a11y` to recognize RMWC input components as controls. // {@link https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md#case-my-label-and-input-components-are-custom-components} 'jsx-a11y/label-has-associated-control': [ diff --git a/app/root.tsx b/app/root.tsx index 42131e8a..246c9b1c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -48,8 +48,6 @@ export const handle: Handle = { breadcrumb: () => nicholas.engineering, } -export const config = { runtime: 'edge' } - function Header() { const matches = useMatches() const user = useOptionalUser() diff --git a/app/routes/og.tsx b/app/routes/og.tsx new file mode 100644 index 00000000..5edc0a39 --- /dev/null +++ b/app/routes/og.tsx @@ -0,0 +1,37 @@ +import { ImageResponse } from '@vercel/og' + +export const config = { runtime: 'edge' } + +export function loader() { + return new ImageResponse( + ( +
+ +
nicholas.engineering
+
+ ), + { + width: 800, + height: 400, + }, + ) +} diff --git a/app/routes/shows.$showId.og.tsx b/app/routes/shows.$showId.og.tsx new file mode 100644 index 00000000..f2bb213d --- /dev/null +++ b/app/routes/shows.$showId.og.tsx @@ -0,0 +1,45 @@ +import { ImageResponse } from '@vercel/og' +import { type LoaderArgs } from '@vercel/remix' + +import { prisma } from 'db.server' +import { log } from 'log.server' + +export const config = { runtime: 'edge' } + +export async function loader({ params }: LoaderArgs) { + log.debug('getting show...') + const showId = Number(params.showId) + if (Number.isNaN(showId)) throw new Response(null, { status: 404 }) + const show = await prisma.show.findUnique({ + where: { id: showId }, + include: { + looks: { include: { image: true }, orderBy: { number: 'asc' }, take: 3 }, + }, + }) + log.debug('got show %o', show) + if (show == null) throw new Response(null, { status: 404 }) + return new ImageResponse( + ( +
+
+
{show.name}
+
+
+ {show.looks.map((look) => ( +
+ +
+ ))} +
+
+ ), + { + width: 800, + height: 400, + }, + ) +} diff --git a/package.json b/package.json index b035c011..ad0d8aa9 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,10 @@ "build:css": "pnpm generate:css --minify", "build:remix": "remix build", "build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build --bundle", - "vercel-build": "replace 'prisma/client' 'prisma/client/edge' app -r && pnpm build", + "vercel-build": "pnpm replace && pnpm build", + "replace": "run-p 'replace:*'", + "replace:prisma": "replace 'prisma/client' 'prisma/client/edge' app -r", + "replace:og": "replace '@m5r/og' '@vercel/og' app -r", "dev": "run-p 'dev:*' | pino-pretty", "dev:prisma": "prisma generate --watch", "dev:build": "cross-env NODE_ENV=development pnpm build:server --watch", @@ -60,6 +63,7 @@ "@remix-run/express": "^1.19.1", "@remix-run/react": "^1.19.1", "@vercel/analytics": "^0.1.11", + "@vercel/og": "^0.5.9", "@vercel/remix": "^1.19.1", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c688c30c..550c7383 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ dependencies: '@vercel/analytics': specifier: ^0.1.11 version: 0.1.11(react@18.2.0) + '@vercel/og': + specifier: ^0.5.9 + version: 0.5.9 '@vercel/remix': specifier: ^1.19.1 version: 1.19.1(react-dom@18.2.0)(react@18.2.0) @@ -4153,6 +4156,11 @@ packages: dependencies: web-streams-polyfill: 3.2.1 + /@resvg/resvg-wasm@2.4.1: + resolution: {integrity: sha512-yi6R0HyHtsoWTRA06Col4WoDs7SvlXU3DLMNP2bdAgs7HK18dTEVl1weXgxRzi8gwLteGUbIg29zulxIB3GSdg==} + engines: {node: '>= 10'} + dev: false + /@rollup/pluginutils@4.2.1: resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -4296,6 +4304,15 @@ packages: - supports-color dev: true + /@shuding/opentype.js@1.4.0-beta.0: + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + dev: false + /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} dependencies: @@ -5031,6 +5048,15 @@ packages: react: 18.2.0 dev: false + /@vercel/og@0.5.9: + resolution: {integrity: sha512-CtjaV/BVHtNCjRtxGqn8Q6AKFLqcG34Byxr91+mY+4eqyp/09LVe9jEeY9WXjbaKvu8syWPMteTpY+YQUQYzSg==} + engines: {node: '>=16'} + dependencies: + '@resvg/resvg-wasm': 2.4.1 + satori: 0.10.1 + yoga-wasm-web: 0.3.3 + dev: false + /@vercel/remix@1.19.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-lIiLbXq5y8g2vQmDkxBsYEWvr38LCqU45ThZ+v1as8P6U6PEBtVFtdWq9YRsG1IaOP15sgJyXglB43gpIJol4Q==} engines: {node: '>=14'} @@ -5557,6 +5583,11 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -5823,6 +5854,10 @@ packages: engines: {node: '>=6'} dev: true + /camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + dev: false + /caniuse-lite@1.0.30001473: resolution: {integrity: sha512-ewDad7+D2vlyy+E4UJuVfiBsU69IL+8oVmTuZnH5Q6CIUbxNfI50uVpRHbUPDD6SUaN2o0Lh4DhTrvLG/Tn1yg==} dev: true @@ -6383,6 +6418,19 @@ packages: type-fest: 1.4.0 dev: true + /css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + dev: false + + /css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + dev: false + + /css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + dev: false + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: @@ -6393,6 +6441,14 @@ packages: nth-check: 2.1.1 dev: true + /css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + dev: false + /css-what@5.1.0: resolution: {integrity: sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==} engines: {node: '>= 6'} @@ -6845,6 +6901,10 @@ packages: resolution: {integrity: sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==} dev: true + /emoji-regex@10.2.1: + resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==} + dev: false + /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true @@ -7933,6 +7993,10 @@ packages: web-streams-polyfill: 3.2.1 dev: true + /fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + dev: false + /figures@2.0.0: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} @@ -8637,6 +8701,11 @@ packages: readable-stream: 3.6.2 dev: false + /hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + dev: false + /hook-std@3.0.0: resolution: {integrity: sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9616,6 +9685,13 @@ packages: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + /linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -11245,7 +11321,6 @@ packages: /pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - dev: true /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -11254,6 +11329,13 @@ packages: callsites: 3.1.0 dev: true + /parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + dev: false + /parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} dependencies: @@ -12775,6 +12857,22 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /satori@0.10.1: + resolution: {integrity: sha512-F4bTCkDp931tLb7+UCNPBuSQwXhikrUkI4fBQo6fA8lF0Evqqgg3nDyUpRktQpR5Ry1DIiIVqLyEwkAms87ykg==} + engines: {node: '>=16'} + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-to-react-native: 3.2.0 + emoji-regex: 10.2.1 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -13320,6 +13418,10 @@ packages: strip-ansi: 7.1.0 dev: true + /string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + dev: false + /string.prototype.matchall@4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} dependencies: @@ -13687,6 +13789,10 @@ packages: globrex: 0.1.2 dev: true + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: false + /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} dev: false @@ -14008,6 +14114,13 @@ packages: engines: {node: '>=4'} dev: true + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: false + /unified@10.1.2: resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} dependencies: @@ -14772,6 +14885,10 @@ packages: engines: {node: '>=12.20'} dev: true + /yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + dev: false + /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} diff --git a/remix.config.js b/remix.config.js index 56547c6b..91e9490f 100644 --- a/remix.config.js +++ b/remix.config.js @@ -14,10 +14,4 @@ module.exports = { ignoredRouteFiles: ['**/.*', '**/*.css', '**/*.test.{js,jsx,ts,tsx}'], serverModuleFormat: 'cjs', serverDependenciesToBundle: ['nanoid/non-secure'], - images: { - sizes: [200, 300, 400, 500, 600, 700, 800, 900, 1000], - domains: ['aritzia.scene7.com'], - minimumCacheTTL: 60, - formats: ['image/webp', 'image/avif'], - }, }