1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-04-26 17:48:30 -04:00

rm middleware.ts 👋

This commit is contained in:
Jake Jarvis 2025-03-30 09:40:43 -04:00
parent 444f91b6dc
commit 50c184fb21
Signed by: jake
SSH Key Fingerprint: SHA256:nCkvAjYA6XaSPUqc4TfbBQTpzr8Xj7ritg/sGInCdkc
6 changed files with 37 additions and 100 deletions

View File

@ -11,7 +11,7 @@ const Analytics = () => {
return ( return (
<Script <Script
src="/_stream/u/script.js" // see middleware rewrite src="/_stream/u/script.js" // see next.config.ts rewrite
id="umami-js" id="umami-js"
strategy="afterInteractive" strategy="afterInteractive"
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID} data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}

View File

@ -33,9 +33,6 @@ export const sendMessage = async (prevState: ContactState, formData: FormData):
try { try {
const data = v.safeParse(ContactSchema, Object.fromEntries(formData)); const data = v.safeParse(ContactSchema, Object.fromEntries(formData));
// send raw valibot result to Sentry for debugging
Sentry.captureMessage(JSON.stringify(data), "debug");
if (!data.success) { if (!data.success) {
return { return {
success: false, success: false,

View File

@ -16,11 +16,11 @@ const ContactForm = () => {
message: "", message: "",
}); });
const [formFields, setFormFields] = useState<ContactInput>({ // keep track of input so we can repopulate the fields if the form fails
const [formFields, setFormFields] = useState<Partial<ContactInput>>({
name: "", name: "",
email: "", email: "",
message: "", message: "",
"cf-turnstile-response": "",
}); });
return ( return (
@ -29,44 +29,43 @@ const ContactForm = () => {
type="text" type="text"
name="name" name="name"
placeholder="Name" placeholder="Name"
// required
value={formFields.name} value={formFields.name}
onChange={(e) => { onChange={(e) => {
setFormFields({ ...formFields, name: e.target.value }); setFormFields({ ...formFields, name: e.target.value });
}} }}
disabled={pending || formState.success} disabled={pending || formState.success}
className={clsx(styles.input, formState.errors?.name && styles.invalid)} className={clsx(styles.input, !pending && formState.errors?.name && styles.invalid)}
/> />
{formState.errors?.name && <span className={styles.fieldError}>{formState.errors.name[0]}</span>} {!pending && formState.errors?.name && <span className={styles.fieldError}>{formState.errors.name[0]}</span>}
<input <input
type="email" type="email"
name="email" name="email"
placeholder="Email" placeholder="Email"
// required
inputMode="email" inputMode="email"
value={formFields.email} value={formFields.email}
onChange={(e) => { onChange={(e) => {
setFormFields({ ...formFields, email: e.target.value }); setFormFields({ ...formFields, email: e.target.value });
}} }}
disabled={pending || formState.success} disabled={pending || formState.success}
className={clsx(styles.input, formState.errors?.email && styles.invalid)} className={clsx(styles.input, !pending && formState.errors?.email && styles.invalid)}
/> />
{formState.errors?.email && <span className={styles.fieldError}>{formState.errors.email[0]}</span>} {!pending && formState.errors?.email && <span className={styles.fieldError}>{formState.errors.email[0]}</span>}
<TextareaAutosize <TextareaAutosize
name="message" name="message"
placeholder="Write something..." placeholder="Write something..."
minRows={5} minRows={5}
// required
value={formFields.message} value={formFields.message}
onChange={(e) => { onChange={(e) => {
setFormFields({ ...formFields, message: e.target.value }); setFormFields({ ...formFields, message: e.target.value });
}} }}
disabled={pending || formState.success} disabled={pending || formState.success}
className={clsx(styles.input, styles.textarea, formState.errors?.message && styles.invalid)} className={clsx(styles.input, styles.textarea, !pending && formState.errors?.message && styles.invalid)}
/> />
{formState.errors?.message && <span className={styles.fieldError}>{formState.errors.message[0]}</span>} {!pending && formState.errors?.message && (
<span className={styles.fieldError}>{formState.errors.message[0]}</span>
)}
<div <div
style={{ style={{
@ -106,12 +105,12 @@ const ContactForm = () => {
<div style={{ margin: "1em 0" }}> <div style={{ margin: "1em 0" }}>
<Turnstile sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "1x00000000000000000000AA"} fixedSize /> <Turnstile sitekey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "1x00000000000000000000AA"} fixedSize />
</div> </div>
{formState.errors?.["cf-turnstile-response"] && ( {!pending && formState.errors?.["cf-turnstile-response"] && (
<span className={styles.fieldError}>{formState.errors["cf-turnstile-response"][0]}</span> <span className={styles.fieldError}>{formState.errors["cf-turnstile-response"][0]}</span>
)} )}
<div className={styles.actionRow}> <div className={styles.actionRow}>
{!formState?.success && ( {!formState.success && (
<button type="submit" disabled={pending} className={styles.submitButton}> <button type="submit" disabled={pending} className={styles.submitButton}>
{pending ? ( {pending ? (
<span>Sending...</span> <span>Sending...</span>
@ -131,7 +130,7 @@ const ContactForm = () => {
)} )}
</button> </button>
)} )}
{formState.message && ( {!pending && formState.message && (
<div className={clsx(styles.result, formState.success ? styles.success : styles.error)}> <div className={clsx(styles.result, formState.success ? styles.success : styles.error)}>
{formState.success ? ( {formState.success ? (
<CheckIcon size="1.3em" className={styles.resultIcon} /> <CheckIcon size="1.3em" className={styles.resultIcon} />

View File

@ -7,7 +7,7 @@ const robots = (): MetadataRoute.Robots => ({
rules: [ rules: [
{ {
userAgent: "*", userAgent: "*",
disallow: ["/_stream/", "/_otel/", "/api/", "/404", "/500"], disallow: ["/_stream/", "/api/", "/pubkey.asc", "/404", "/500"],
}, },
], ],
sitemap: `${BASE_URL}/sitemap.xml`, sitemap: `${BASE_URL}/sitemap.xml`,

View File

@ -1,66 +0,0 @@
import { NextResponse } from "next/server";
import type { NextRequest, MiddlewareConfig } from "next/server";
// assign "short codes" to approved reverse proxy destinations. for example:
// ["abc", "https://jakejarvis.github.io"] => /_stream/abc/123.html -> https://jakejarvis.github.io/123.html
const rewritePrefix = "/_stream/";
const rewrites = new Map();
// umami backend, see https://umami.is/docs/guides/running-on-vercel#proxy-umami-analytics-via-vercel
if (process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID) {
rewrites.set("u", process.env.NEXT_PUBLIC_UMAMI_URL || "https://cloud.umami.is");
}
export const middleware = (request: NextRequest) => {
const headers = new Headers();
// https://community.torproject.org/onion-services/advanced/onion-location/
if (process.env.NEXT_PUBLIC_ONION_DOMAIN) {
const onionUrl = request.nextUrl.clone();
onionUrl.hostname = process.env.NEXT_PUBLIC_ONION_DOMAIN;
onionUrl.protocol = "http";
onionUrl.port = "";
headers.set("onion-location", onionUrl.toString());
}
// debugging 🥛
headers.set("x-got-milk", "2%");
if (request.nextUrl.pathname.startsWith(rewritePrefix)) {
// extract the short code
const pathAfterPrefix = request.nextUrl.pathname.slice(rewritePrefix.length);
const slashIndex = pathAfterPrefix.indexOf("/");
const key = slashIndex === -1 ? pathAfterPrefix : pathAfterPrefix.slice(0, slashIndex);
// search the rewrite map for the short code
const proxiedOrigin = rewrites.get(key);
if (proxiedOrigin) {
// it's now safe to build the rewritten URL
const proxiedPath = slashIndex === -1 ? "/" : pathAfterPrefix.slice(slashIndex);
const proxiedUrl = new URL(`${proxiedPath}${request.nextUrl.search}`, proxiedOrigin);
headers.set("x-rewrite-url", proxiedUrl.toString());
// finally do the rewriting
return NextResponse.rewrite(proxiedUrl, {
request,
headers,
});
}
}
// if we've gotten this far, continue normally to next.js
return NextResponse.next({
request,
headers,
});
};
export const config: MiddlewareConfig = {
// save compute time by skipping middleware for next.js internals and static files
matcher: [
"/((?!_next/static|_next/image|_vercel|_otel|api|\\.well-known|[^?]*\\.(?:png|jpe?g|gif|webp|avif|svg|ico|webm|mp4|ttf|woff2?|xml|atom|txt|pdf|webmanifest)).*)",
],
};

View File

@ -5,12 +5,6 @@ import { visit } from "unist-util-visit";
import * as mdxPlugins from "./lib/helpers/remark-rehype-plugins"; import * as mdxPlugins from "./lib/helpers/remark-rehype-plugins";
import type { NextConfig } from "next"; import type { NextConfig } from "next";
type NextPlugin = (
config: NextConfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options?: any
) => NextConfig;
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactStrictMode: true, reactStrictMode: true,
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true,
@ -60,10 +54,19 @@ const nextConfig: NextConfig = {
serverSourceMaps: true, serverSourceMaps: true,
}, },
eslint: { eslint: {
// https://nextjs.org/docs/basic-features/eslint#linting-custom-directories-and-files
dirs: ["app", "components", "contexts", "hooks", "lib", "notes"], dirs: ["app", "components", "contexts", "hooks", "lib", "notes"],
}, },
headers: async () => [ headers: async () => [
{
// matches any path without a file extension (aka period) or an underscore (e.g. /_next/image)
source: "/:path([^._]*)",
headers: [
{
key: "onion-location",
value: `http://${process.env.NEXT_PUBLIC_ONION_DOMAIN}/:path`,
},
],
},
{ {
source: "/pubkey.asc", source: "/pubkey.asc",
headers: [ headers: [
@ -75,6 +78,11 @@ const nextConfig: NextConfig = {
}, },
], ],
rewrites: async () => [ rewrites: async () => [
{
// https://umami.is/docs/guides/running-on-vercel#proxy-umami-analytics-via-vercel
source: "/_stream/u/:path*",
destination: `${process.env.NEXT_PUBLIC_UMAMI_URL || "https://cloud.umami.is"}/:path*`,
},
{ {
// https://github.com/jakejarvis/tweets // https://github.com/jakejarvis/tweets
source: "/tweets/:path*", source: "/tweets/:path*",
@ -150,8 +158,10 @@ const nextConfig: NextConfig = {
}; };
// my own macgyvered version of next-compose-plugins (RIP) // my own macgyvered version of next-compose-plugins (RIP)
// eslint-disable-next-line @typescript-eslint/no-explicit-any const nextPlugins: Array<
const nextPlugins: Array<NextPlugin | [NextPlugin, any]> = [ // eslint-disable-next-line @typescript-eslint/no-explicit-any
(config: NextConfig) => NextConfig | [(config: NextConfig) => NextConfig, any]
> = [
require("@next/bundle-analyzer")({ require("@next/bundle-analyzer")({
enabled: !!process.env.ANALYZE, enabled: !!process.env.ANALYZE,
}), }),
@ -208,13 +218,10 @@ const nextPlugins: Array<NextPlugin | [NextPlugin, any]> = [
project: process.env.SENTRY_PROJECT, project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN, authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.CI, silent: !process.env.CI,
tunnelRoute: "/_otel", // ensure this path is included in middleware's negative config.matcher expression tunnelRoute: "/_stream/otel",
widenClientFileUpload: true, widenClientFileUpload: true,
disableLogger: true, disableLogger: true,
telemetry: false, telemetry: false,
autoInstrumentAppDirectory: true,
autoInstrumentServerFunctions: true,
autoInstrumentMiddleware: false,
bundleSizeOptimizations: { bundleSizeOptimizations: {
excludeDebugStatements: true, excludeDebugStatements: true,
}, },
@ -226,8 +233,8 @@ const nextPlugins: Array<NextPlugin | [NextPlugin, any]> = [
export default (): NextConfig => export default (): NextConfig =>
nextPlugins.reduce((acc, next) => { nextPlugins.reduce((acc, next) => {
if (Array.isArray(next)) { if (Array.isArray(next)) {
return (next[0] as NextPlugin)(acc, next[1]); return next[0](acc, next[1]);
} }
return (next as NextPlugin)(acc); return next(acc);
}, nextConfig); }, nextConfig);