From ec7c9fae549d12d7cd454076f8691357619a84e4 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Mon, 31 Mar 2025 09:15:40 -0400 Subject: [PATCH] more error handling --- app/contact/actions.ts | 33 ++++++++++++-- app/contact/page.tsx | 2 +- app/notes/[slug]/counter.tsx | 2 + app/page.tsx | 5 +-- app/projects/page.tsx | 16 ++++++- app/robots.ts | 2 +- components/Gist/Gist.tsx | 8 +++- components/YouTube/YouTube.module.css | 4 -- components/YouTube/YouTube.tsx | 19 ++++---- instrumentation-client.ts | 1 - instrumentation.ts | 1 - next.config.ts | 40 ++++++++++------- package.json | 6 +-- pnpm-lock.yaml | 54 ++++++++++------------ public/pubkey.asc | 64 --------------------------- 15 files changed, 114 insertions(+), 143 deletions(-) delete mode 100644 components/YouTube/YouTube.module.css delete mode 100644 public/pubkey.asc diff --git a/app/contact/actions.ts b/app/contact/actions.ts index 43470a0c..07b066eb 100644 --- a/app/contact/actions.ts +++ b/app/contact/actions.ts @@ -7,10 +7,35 @@ import * as Sentry from "@sentry/nextjs"; import * as config from "../../lib/config"; const ContactSchema = v.object({ - name: v.pipe(v.string(), v.nonEmpty("Your name is required.")), - email: v.pipe(v.string(), v.nonEmpty("Your email address is required."), v.email("Invalid email address.")), - message: v.pipe(v.string(), v.nonEmpty("A message is required.")), - "cf-turnstile-response": v.pipe(v.string(), v.nonEmpty("Just do the stinkin CAPTCHA! 🤖")), + // TODO: replace duplicate error messages with v.message() when released. see: + // https://valibot.dev/api/message/ + // https://github.com/fabian-hiller/valibot/blob/main/library/src/methods/message/message.ts + name: v.pipe(v.string("Your name is required."), v.trim(), v.nonEmpty("Your name is required.")), + email: v.pipe( + v.string("Your email address is required."), + v.trim(), + v.nonEmpty("Your email address is required."), + v.email("Invalid email address.") + ), + message: v.pipe( + v.string("A message is required."), + v.trim(), + v.nonEmpty("A message is required."), + v.minLength(10, "Your message must be at least 10 characters.") + ), + "cf-turnstile-response": v.pipe( + // token wasn't submitted at _all_, most likely a direct POST request by a spam bot + v.string("Shoo, bot."), + // form submitted properly but token was missing, might be a forgetful human + v.nonEmpty("Just do the stinkin CAPTCHA, human! 🤖"), + // very rudimentary length check based on Cloudflare's docs + // https://developers.cloudflare.com/turnstile/troubleshooting/testing/ + v.minLength("XXXX.DUMMY.TOKEN.XXXX".length), + // "A Turnstile token can have up to 2048 characters." + // https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ + v.maxLength(2048), + v.readonly() + ), }); export type ContactInput = v.InferInput; diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 879b81f6..3ea7d1ce 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -28,7 +28,7 @@ const Page = () => {

🔐 You can grab my public key here:{" "} - + 6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39 diff --git a/app/notes/[slug]/counter.tsx b/app/notes/[slug]/counter.tsx index b54d2d30..a7fdfbc3 100644 --- a/app/notes/[slug]/counter.tsx +++ b/app/notes/[slug]/counter.tsx @@ -23,6 +23,8 @@ const HitCounter = async ({ slug }: { slug: string }) => { ); } catch (error) { Sentry.captureException(error); + + return ?; } }; diff --git a/app/page.tsx b/app/page.tsx index 3fb32aa3..5e3572bf 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -258,13 +258,12 @@ const Page = () => { {" "} {" "} => { accept: "application/vnd.github.v3+json", authorization: `token ${process.env.GITHUB_TOKEN}`, }, + request: { + // override fetch() to use next's extension to cache the response + // https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options + fetch: (url: string | URL | Request, options?: RequestInit) => { + return fetch(url, { + ...options, + cache: "force-cache", + next: { + // 10 minutes + revalidate: 600, + }, + }); + }, + }, } ); diff --git a/app/robots.ts b/app/robots.ts index ebd770f0..5b2a3f7f 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -7,7 +7,7 @@ const robots = (): MetadataRoute.Robots => ({ rules: [ { userAgent: "*", - disallow: ["/_stream/", "/api/", "/pubkey.asc", "/404", "/500"], + disallow: ["/_stream/", "/api/", "/404", "/500"], }, ], sitemap: `${BASE_URL}/sitemap.xml`, diff --git a/components/Gist/Gist.tsx b/components/Gist/Gist.tsx index 4963ec5d..be2d3d44 100644 --- a/components/Gist/Gist.tsx +++ b/components/Gist/Gist.tsx @@ -9,7 +9,13 @@ const Gist = async ({ id, file }: GistProps) => { const iframeId = `gist-${id}${file ? `-${file}` : ""}`; const scriptUrl = `https://gist.github.com/${id}.js${file ? `?file=${file}` : ""}`; - const scriptResponse = await fetch(scriptUrl); + const scriptResponse = await fetch(scriptUrl, { + cache: "force-cache", + next: { + // cache indefinitely in data store + revalidate: 0, + }, + }); if (!scriptResponse.ok) { console.warn(`[gist] failed to fetch js:`, scriptResponse.statusText); diff --git a/components/YouTube/YouTube.module.css b/components/YouTube/YouTube.module.css deleted file mode 100644 index 5961175c..00000000 --- a/components/YouTube/YouTube.module.css +++ /dev/null @@ -1,4 +0,0 @@ -/* stylelint-disable-next-line selector-type-no-unknown */ -.wrapper lite-youtube { - margin: 0 auto; -} diff --git a/components/YouTube/YouTube.tsx b/components/YouTube/YouTube.tsx index 54108e7f..8fab3c98 100644 --- a/components/YouTube/YouTube.tsx +++ b/components/YouTube/YouTube.tsx @@ -1,17 +1,14 @@ -import { YouTubeEmbed } from "@next/third-parties/google"; +"use client"; -import styles from "./YouTube.module.css"; +import YouTubeEmbed from "react-lite-youtube-embed"; +import type { ComponentPropsWithoutRef } from "react"; -export type YouTubeProps = { - id: string; -}; +import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css"; -const YouTube = ({ id }: YouTubeProps) => { - return ( -

- -
- ); +export type YouTubeProps = Omit, "title">; + +const YouTube = ({ ...rest }: YouTubeProps) => { + return ; }; export default YouTube; diff --git a/instrumentation-client.ts b/instrumentation-client.ts index 8e5ba2b3..57559894 100644 --- a/instrumentation-client.ts +++ b/instrumentation-client.ts @@ -2,7 +2,6 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - environment: process.env.NEXT_PUBLIC_VERCEL_ENV, integrations: [Sentry.browserTracingIntegration(), Sentry.httpClientIntegration()], tracesSampleRate: 1.0, }); diff --git a/instrumentation.ts b/instrumentation.ts index b52175bd..ed7d18e9 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -5,7 +5,6 @@ export const onRequestError = Sentry.captureRequestError; export const register = () => { Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN, - environment: process.env.NEXT_PUBLIC_VERCEL_ENV, integrations: [Sentry.captureConsoleIntegration()], tracesSampleRate: 1.0, // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#normalizeDepth diff --git a/next.config.ts b/next.config.ts index a1c05266..4bb5dcc5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -58,7 +58,23 @@ const nextConfig: NextConfig = { }, headers: async () => [ { - // matches any path without a file extension (aka period) or an underscore (e.g. /_next/image) + // matches any path + source: "/(.*)", + headers: [ + { + key: "strict-transport-security", + value: "max-age=63072000; includeSubDomains; preload", + }, + { + key: "x-got-milk", + value: "2%", + }, + ], + }, + { + // https://community.torproject.org/onion-services/advanced/onion-location/ + // only needed on actual pages, not static assets, so make a best effort by matching any path **without** a file + // extension (aka a period) and/or an underscore (e.g. /_next/image). source: "/:path([^._]*)", headers: [ { @@ -67,15 +83,6 @@ const nextConfig: NextConfig = { }, ], }, - { - source: "/pubkey.asc", - headers: [ - { - key: "Content-Type", - value: "text/plain; charset=utf-8", - }, - ], - }, ], rewrites: async () => [ { @@ -88,6 +95,11 @@ const nextConfig: NextConfig = { source: "/tweets/:path*", destination: "https://tweets-khaki.vercel.app/:path*", }, + { + source: "/pubkey.asc", + destination: + "https://keys.openpgp.org/pks/lookup?op=get&options=mr&search=0x3bc6e5776bf379d36f6714802b0c9cf251e69a39", + }, ], redirects: async () => [ { source: "/y2k", destination: "https://y2k.pages.dev", permanent: false }, @@ -231,10 +243,4 @@ const nextPlugins: Array< // eslint-disable-next-line import/no-anonymous-default-export export default (): NextConfig => - nextPlugins.reduce((acc, next) => { - if (Array.isArray(next)) { - return next[0](acc, next[1]); - } - - return next(acc); - }, nextConfig); + nextPlugins.reduce((acc, plugin) => (Array.isArray(plugin) ? plugin[0](acc, plugin[1]) : plugin(acc)), nextConfig); diff --git a/package.json b/package.json index f015d757..fac11a74 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@mdx-js/react": "^3.1.0", "@next/bundle-analyzer": "15.3.0-canary.25", "@next/mdx": "15.3.0-canary.25", - "@next/third-parties": "15.3.0-canary.25", "@octokit/graphql": "^8.2.1", "@octokit/graphql-schema": "^15.26.0", "@sentry/nextjs": "^9.10.1", @@ -37,7 +36,7 @@ "fast-glob": "^3.3.3", "feed": "^4.2.2", "geist": "^1.3.1", - "html-entities": "^2.5.5", + "html-entities": "^2.6.0", "lucide-react": "0.485.0", "modern-normalize": "^3.0.1", "next": "15.3.0-canary.25", @@ -49,8 +48,9 @@ "react-dom": "19.1.0", "react-innertext": "^1.1.5", "react-is": "19.1.0", + "react-lite-youtube-embed": "^2.4.0", "react-schemaorg": "^2.0.0", - "react-textarea-autosize": "^8.5.8", + "react-textarea-autosize": "^8.5.9", "react-timeago": "^8.0.0", "react-turnstile": "^1.1.4", "react-tweet": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7c460bf..28478a7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,9 +32,6 @@ importers: '@next/mdx': specifier: 15.3.0-canary.25 version: 15.3.0-canary.25(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.98.0))(@mdx-js/react@3.1.0(@types/react@19.0.12)(react@19.1.0)) - '@next/third-parties': - specifier: 15.3.0-canary.25 - version: 15.3.0-canary.25(next@15.3.0-canary.25(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-aeaed83-20250323)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) '@octokit/graphql': specifier: ^8.2.1 version: 8.2.1 @@ -69,8 +66,8 @@ importers: specifier: ^1.3.1 version: 1.3.1(next@15.3.0-canary.25(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-aeaed83-20250323)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) html-entities: - specifier: ^2.5.5 - version: 2.5.5 + specifier: ^2.6.0 + version: 2.6.0 lucide-react: specifier: 0.485.0 version: 0.485.0(react@19.1.0) @@ -104,12 +101,15 @@ importers: react-is: specifier: 19.1.0 version: 19.1.0 + react-lite-youtube-embed: + specifier: ^2.4.0 + version: 2.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-schemaorg: specifier: ^2.0.0 version: 2.0.0(react@19.1.0)(schema-dts@1.1.5)(typescript@5.8.2) react-textarea-autosize: - specifier: ^8.5.8 - version: 8.5.8(@types/react@19.0.12)(react@19.1.0) + specifier: ^8.5.9 + version: 8.5.9(@types/react@19.0.12)(react@19.1.0) react-timeago: specifier: ^8.0.0 version: 8.0.0(react@19.1.0) @@ -728,12 +728,6 @@ packages: cpu: [x64] os: [win32] - '@next/third-parties@15.3.0-canary.25': - resolution: {integrity: sha512-UiwiSeKKyBW31YZ6v4QEb9+Sx22ubWcf/74Jv1dIQnXrKLMd3OTNj9r0Z3Bj5sIpuBx0NIf8LCr90H2sJRRYDw==} - peerDependencies: - next: ^13.0.0 || ^14.0.0 || ^15.0.0 - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2687,8 +2681,8 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - html-entities@2.5.5: - resolution: {integrity: sha512-24CG9o869vSa86BGCf7x65slrAztzFTU5VBQzEIwqjhKuB4zCC7xlH/7NCcZ1EN5MdmGx9lUqugfutuT6J+jKQ==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3702,6 +3696,12 @@ packages: react-is@19.1.0: resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-lite-youtube-embed@2.4.0: + resolution: {integrity: sha512-Xo6cM1zPlROvvM97JkqQIoXstlQDaC4+DawmM7BB7Hh1cXrkBHEGq1iJlQxBTUWAUklmpcC7ph7qg7CztXtABQ==} + peerDependencies: + react: '>=18.2.0' + react-dom: '>=18.2.0' + react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} @@ -3713,8 +3713,8 @@ packages: schema-dts: '>=0.7.4' typescript: '>=3.1.6' - react-textarea-autosize@8.5.8: - resolution: {integrity: sha512-iUiIj70JefrTuSJ4LbVFiSqWiHHss5L63L717bqaWHMgkm9sz6eEvro4vZ3uQfGJbevzwT6rHOszHKA8RkhRMg==} + react-textarea-autosize@8.5.9: + resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4246,9 +4246,6 @@ packages: engines: {node: '>=10'} hasBin: true - third-party-capital@1.0.20: - resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==} - tinyglobby@0.2.12: resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} engines: {node: '>=12.0.0'} @@ -5081,12 +5078,6 @@ snapshots: '@next/swc-win32-x64-msvc@15.3.0-canary.25': optional: true - '@next/third-parties@15.3.0-canary.25(next@15.3.0-canary.25(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-aeaed83-20250323)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': - dependencies: - next: 15.3.0-canary.25(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-aeaed83-20250323)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react: 19.1.0 - third-party-capital: 1.0.20 - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7460,7 +7451,7 @@ snapshots: dependencies: lru-cache: 10.4.3 - html-entities@2.5.5: {} + html-entities@2.6.0: {} html-escaper@2.0.2: {} @@ -8727,6 +8718,11 @@ snapshots: react-is@19.1.0: {} + react-lite-youtube-embed@2.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -8737,7 +8733,7 @@ snapshots: schema-dts: 1.1.5 typescript: 5.8.2 - react-textarea-autosize@8.5.8(@types/react@19.0.12)(react@19.1.0): + react-textarea-autosize@8.5.9(@types/react@19.0.12)(react@19.1.0): dependencies: '@babel/runtime': 7.27.0 react: 19.1.0 @@ -9527,8 +9523,6 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - third-party-capital@1.0.20: {} - tinyglobby@0.2.12: dependencies: fdir: 6.4.3(picomatch@4.0.2) diff --git a/public/pubkey.asc b/public/pubkey.asc deleted file mode 100644 index fd63d34d..00000000 --- a/public/pubkey.asc +++ /dev/null @@ -1,64 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -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 -=sqOH ------END PGP PUBLIC KEY BLOCK-----