diff --git a/app/contact/action.ts b/app/contact/action.ts index 4dd8612a..06a0eef7 100644 --- a/app/contact/action.ts +++ b/app/contact/action.ts @@ -72,7 +72,7 @@ export const send = async (state: ContactState, payload: FormData): Promise {
- +
{!pending && formState.errors?.["cf-turnstile-response"] && ( {formState.errors["cf-turnstile-response"][0]} diff --git a/app/notes/[slug]/opengraph-image.tsx b/app/notes/[slug]/opengraph-image.tsx index bb1c6852..c4f6182d 100644 --- a/app/notes/[slug]/opengraph-image.tsx +++ b/app/notes/[slug]/opengraph-image.tsx @@ -1,10 +1,11 @@ +import { env } from "../../../lib/env"; import { ImageResponse } from "next/og"; import { notFound } from "next/navigation"; -import { join } from "path"; -import { existsSync } from "fs"; -import { readFile } from "fs/promises"; +import path from "path"; +import fs from "fs"; import { getSlugs, getFrontMatter } from "../../../lib/helpers/posts"; -import { POSTS_DIR, AVATAR_PATH } from "../../../lib/config/constants"; +import * as config from "../../../lib/config"; +import { POSTS_DIR } from "../../../lib/config/constants"; export const contentType = "image/png"; export const size = { @@ -29,10 +30,10 @@ const getLocalImage = async (src: string): Promise => { // https://stackoverflow.com/questions/5775469/whats-the-valid-way-to-include-an-image-with-no-src/14115340#14115340 const NO_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; - const imagePath = join(process.cwd(), src); + const imagePath = path.join(process.cwd(), src); try { - if (!existsSync(imagePath)) { + if (!fs.existsSync(imagePath)) { console.error(`[og-image] couldn't find an image file located at "${imagePath}"`); // return a 1x1 transparent gif if the image doesn't exist instead of crashing @@ -40,7 +41,7 @@ const getLocalImage = async (src: string): Promise => { } // return the raw image data as a buffer - return Uint8Array.from(await readFile(imagePath)).buffer; + return Uint8Array.from(await fs.promises.readFile(imagePath)).buffer; } catch (error) { console.error(`[og-image] found "${imagePath}" but couldn't read it:`, error); @@ -56,6 +57,16 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> }) // get the post's title and image filename from its frontmatter const frontmatter = await getFrontMatter(slug); + const [postImg, avatarImg, fontRegular, fontSemiBold] = await Promise.all([ + frontmatter!.image ? getLocalImage(`${POSTS_DIR}/${slug}/${frontmatter!.image}`) : null, + + // IMPORTANT: include these exact paths in next.config.ts under "outputFileTracingIncludes" + getLocalImage("app/avatar.jpg"), + // load the Geist font directly from its npm package + fs.promises.readFile(path.join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-Regular.ttf")), + fs.promises.readFile(path.join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")), + ]); + // template is HEAVILY inspired by https://og-new.clerkstage.dev/ return new ImageResponse( ( @@ -91,25 +102,6 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> }) }} > -
- -
-
}) flexDirection: "column", rowGap: "1.5rem", flexShrink: 0, - paddingTop: "2.5rem", - // don't wrap the title text at 50% if there's no image to leave room for - width: frontmatter!.image ? "50%" : "100%", + paddingTop: "2rem", + // don't wrap the title text if there's no image to leave room for + width: postImg ? "35%" : "100%", + marginRight: "0.75rem", }} >
+ {avatarImg && ( + + )} - Notes + {config.siteName}
@@ -169,6 +169,31 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> }) {frontmatter!.title}
+
+ + {POSTS_DIR.charAt(0).toUpperCase() + POSTS_DIR.slice(1)} + +
+
}) lineHeight: "1.2", }} > - {new Date(frontmatter!.date).toLocaleDateString("en-US", { + {new Date(frontmatter!.date).toLocaleDateString(env.NEXT_PUBLIC_SITE_LOCALE, { year: "numeric", month: "long", day: "numeric", @@ -189,19 +214,23 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> })
- {frontmatter!.image && ( + {postImg && (
)} @@ -213,15 +242,13 @@ const OpenGraphImage = async ({ params }: { params: Promise<{ slug: string }> }) fonts: [ { name: "Geist-Regular", - // load the Geist font directly from its npm package - // IMPORTANT: include this exact path in next.config.ts under "outputFileTracingIncludes" - data: await readFile(join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-Regular.ttf")), + data: fontRegular, style: "normal", weight: 400, }, { name: "Geist-SemiBold", - data: await readFile(join(process.cwd(), "node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf")), + data: fontSemiBold, style: "normal", weight: 700, }, diff --git a/components/Code/Code.module.css b/components/Code/Code.module.css index 56dd46c3..5612590c 100644 --- a/components/Code/Code.module.css +++ b/components/Code/Code.module.css @@ -2,11 +2,12 @@ * Inline code **/ -:not([data-rehype-pretty-code-figure]) .code { +.code { padding: 0.2em 0.3em; font-size: 0.925em; + tab-size: 2px; page-break-inside: avoid; - background-color: var(--colors-background-outer); + background-color: var(--colors-background-header); border: 1px solid var(--colors-kinda-light); border-radius: 0.6em; } @@ -15,45 +16,41 @@ * Syntax-highlighted code blocks **/ -[data-rehype-pretty-code-figure]:has(.code) { - margin: 1em auto; +figure:has(.code) { + margin: 1em 0; position: relative; width: 100%; - background-color: var(--colors-background-header); + border-radius: 0.6em; } -[data-rehype-pretty-code-figure] .code { +figure .code { display: block; overflow-x: auto; padding: 1em; - font-size: 0.9em; - tab-size: 2px; - border: 1px solid var(--colors-kinda-light); - border-radius: 0.6em; counter-reset: line; } -[data-rehype-pretty-code-figure] .code [style*="--shiki"] { +figure .code [style*="--shiki"] { color: var(--shiki-light); font-style: var(--shiki-light-font-style); font-weight: var(--shiki-light-font-weight); text-decoration: var(--shiki-light-text-decoration); } -[data-theme="dark"] [data-rehype-pretty-code-figure] .code [style*="--shiki"] { +[data-theme="dark"] figure .code [style*="--shiki"] { color: var(--shiki-dark); font-style: var(--shiki-dark-font-style); font-weight: var(--shiki-dark-font-weight); text-decoration: var(--shiki-dark-text-decoration); } -[data-rehype-pretty-code-figure] .code[data-line-numbers] > [data-line]:nth-of-type(1), -[data-rehype-pretty-code-figure] .code[data-line-numbers] > [data-line]:nth-of-type(2) { +figure .code > [data-line]:nth-of-type(1), +figure .code > [data-line]:nth-of-type(2) { /* excessive right padding to prevent copy button from covering the first two lines of code */ padding-right: 4em; } -[data-rehype-pretty-code-figure] .code[data-line-numbers] > [data-line]::before { +figure .code[data-line-numbers] > [data-line]::before { display: inline-block; width: 1em; margin-right: 1.5em; @@ -64,11 +61,11 @@ content: counter(line); } -[data-rehype-pretty-code-figure] .code[data-line-numbers-max-digits="2"] > [data-line]::before { +figure .code[data-line-numbers-max-digits="2"] > [data-line]::before { width: 1.25rem; } -[data-rehype-pretty-code-figure] .code[data-line-numbers-max-digits="3"] > [data-line]::before { +figure .code[data-line-numbers-max-digits="3"] > [data-line]::before { width: 1.75rem; } diff --git a/lib/config/constants.ts b/lib/config/constants.ts index 7c3e1cb7..50f90aa1 100644 --- a/lib/config/constants.ts +++ b/lib/config/constants.ts @@ -1,11 +1,5 @@ /** Path to directory with .mdx files, relative to project root. */ export const POSTS_DIR = "notes"; -/** - * Path to an image used in various places to represent the site, relative to project root. This path must be included - * in [next.config.ts](../../next.config.ts) under `outputFileTracingIncludes`. - */ -export const AVATAR_PATH = "app/avatar.jpg"; - /** Maximum width of content wrapper (e.g. for images) in pixels. */ export const MAX_WIDTH = 865; diff --git a/lib/env.ts b/lib/env.ts index d3264e83..fae63210 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -112,16 +112,16 @@ export const env = createEnv({ * * @see https://developers.cloudflare.com/turnstile/troubleshooting/testing/ */ - NEXT_PUBLIC_TURNSTILE_SITE_KEY: v.optional(v.string(), "XXXX.DUMMY.TOKEN.XXXX"), + NEXT_PUBLIC_TURNSTILE_SITE_KEY: v.optional(v.string(), "1x00000000000000000000AA"), }, experimental__runtimeEnv: { NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL || // Vercel: https://vercel.com/docs/environment-variables/system-environment-variables (process.env.VERCEL - ? process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL + ? process.env.VERCEL_ENV === "production" ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` - : process.env.VERCEL_ENV === "preview" && process.env.VERCEL_BRANCH_URL + : process.env.VERCEL_ENV === "preview" ? `https://${process.env.VERCEL_BRANCH_URL}` : process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` @@ -129,7 +129,7 @@ export const env = createEnv({ : undefined) || // Netlify: https://docs.netlify.com/configure-builds/environment-variables/#read-only-variables (process.env.NETLIFY - ? process.env.CONTEXT === "production" && process.env.URL + ? process.env.CONTEXT === "production" ? `${process.env.URL}` : process.env.DEPLOY_PRIME_URL ? `${process.env.DEPLOY_PRIME_URL}` diff --git a/lib/helpers/posts.ts b/lib/helpers/posts.ts index 83245a65..7ea7828a 100644 --- a/lib/helpers/posts.ts +++ b/lib/helpers/posts.ts @@ -97,7 +97,7 @@ Promise => { ) as FrontMatter[]; } - throw new Error(`getFrontMatter() called with invalid argument.`); + throw new Error("getFrontMatter() called with invalid argument."); }; /** Returns the content of a post with very limited processing to include in RSS feeds */