diff --git a/app/(docs)/docs/[[...slug]]/head.tsx b/app/(docs)/docs/[[...slug]]/head.tsx index 1d5974c..c54e4f5 100644 --- a/app/(docs)/docs/[[...slug]]/head.tsx +++ b/app/(docs)/docs/[[...slug]]/head.tsx @@ -1,5 +1,5 @@ import MdxHead from "@/components/docs/mdx-head" export default function Head({ params }) { - return + return } diff --git a/app/(docs)/guides/[...slug]/head.tsx b/app/(docs)/guides/[...slug]/head.tsx index 1d5974c..210e8b2 100644 --- a/app/(docs)/guides/[...slug]/head.tsx +++ b/app/(docs)/guides/[...slug]/head.tsx @@ -1,5 +1,5 @@ import MdxHead from "@/components/docs/mdx-head" export default function Head({ params }) { - return + return } diff --git a/app/(marketing)/blog/[...slug]/head.tsx b/app/(marketing)/blog/[...slug]/head.tsx index 1d5974c..add910d 100644 --- a/app/(marketing)/blog/[...slug]/head.tsx +++ b/app/(marketing)/blog/[...slug]/head.tsx @@ -1,5 +1,12 @@ import MdxHead from "@/components/docs/mdx-head" export default function Head({ params }) { - return + return ( + + ) } diff --git a/assets/fonts/Inter-Bold.ttf b/assets/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..8e82c70 Binary files /dev/null and b/assets/fonts/Inter-Bold.ttf differ diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..8d4eebf Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/components/docs/mdx-head.tsx b/components/docs/mdx-head.tsx index 49a88e6..8d12bbd 100644 --- a/components/docs/mdx-head.tsx +++ b/components/docs/mdx-head.tsx @@ -1,12 +1,15 @@ +import * as z from "zod" import { allDocuments } from "contentlayer/generated" +import { ogImageSchema } from "@/lib/validations/og" interface MdxHeadProps { params: { slug?: string[] } + og?: z.infer } -export default function MdxHead({ params }: MdxHeadProps) { +export default function MdxHead({ params, og }: MdxHeadProps) { const slug = params?.slug?.join("/") || "" const mdxDoc = allDocuments.find((doc) => doc.slugAsParams === slug) @@ -15,6 +18,14 @@ export default function MdxHead({ params }: MdxHeadProps) { } const title = `${mdxDoc.title} - Taxonomy` + const url = process.env.NEXT_PUBLIC_APP_URL + let ogUrl = new URL(`${url}/og.jpg`) + + if (og.type) { + ogUrl = new URL(url) + ogUrl.searchParams.set("heading", mdxDoc.title) + ogUrl.searchParams.set("type", og.type) + } return ( <> @@ -23,11 +34,11 @@ export default function MdxHead({ params }: MdxHeadProps) { - - + + - - + + > ) } diff --git a/lib/validations/og.ts b/lib/validations/og.ts new file mode 100644 index 0000000..fcd3125 --- /dev/null +++ b/lib/validations/og.ts @@ -0,0 +1,6 @@ +import * as z from "zod" + +export const ogImageSchema = z.object({ + heading: z.string(), + type: z.string(), +}) diff --git a/package.json b/package.json index ad4e055..f0d2471 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-popover": "^1.0.2", "@radix-ui/react-toggle": "^1.0.0", "@vercel/analytics": "^0.1.3", + "@vercel/og": "^0.0.21", "clsx": "^1.2.1", "contentlayer": "^0.2.9", "date-fns": "^2.29.3", diff --git a/pages/api/og.tsx b/pages/api/og.tsx new file mode 100644 index 0000000..4236c3e --- /dev/null +++ b/pages/api/og.tsx @@ -0,0 +1,143 @@ +import { ogImageSchema } from "@/lib/validations/og" +import { ImageResponse } from "@vercel/og" +import { NextRequest } from "next/server" + +export const config = { + runtime: "experimental-edge", +} + +const interRegular = fetch( + new URL("../../assets/fonts/Inter-Regular.ttf", import.meta.url) +).then((res) => res.arrayBuffer()) + +const interBold = fetch( + new URL("../../assets/fonts/Inter-Bold.ttf", import.meta.url) +).then((res) => res.arrayBuffer()) + +export default async function handler(req: NextRequest) { + try { + const fontRegular = await interRegular + const fontBold = await interBold + + const url = new URL(req.url) + const values = ogImageSchema.parse(Object.fromEntries(url.searchParams)) + const heading = + values.heading.length > 140 + ? `${values.heading.substring(0, 140)}...` + : values.heading + + const fontSize = heading.length > 100 ? "70px" : "100px" + + return new ImageResponse( + ( + + + + + + + + + + + + + + + + + + {values.type} + + + {heading} + + + + + tx.shadcn.com + + + + + + + github.com/shadcn/taxonomy + + + + ), + { + width: 1200, + height: 630, + fonts: [ + { + name: "Inter", + data: fontRegular, + weight: 400, + style: "normal", + }, + { + name: "Inter", + data: fontBold, + weight: 700, + style: "normal", + }, + ], + } + ) + } catch (error) { + return new Response(`Failed to generate image`, { + status: 500, + }) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e62f8e0..afeccb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: "@types/react": 18.0.15 "@types/react-dom": 18.0.6 "@vercel/analytics": ^0.1.3 + "@vercel/og": ^0.0.21 autoprefixer: ^10.4.8 clsx: ^1.2.1 contentlayer: ^0.2.9 @@ -89,6 +90,7 @@ dependencies: "@radix-ui/react-popover": 1.0.2_bb2bxwco6ptpubzwpazr52qf6i "@radix-ui/react-toggle": 1.0.1_biqbaboplfbrettd7655fr4n2y "@vercel/analytics": 0.1.5_react@18.2.0 + "@vercel/og": 0.0.21 clsx: 1.2.1 contentlayer: 0.2.9 date-fns: 2.29.3 @@ -2043,6 +2045,14 @@ packages: react: 18.2.0 dev: false + /@resvg/resvg-wasm/2.0.0-alpha.4: + resolution: + { + integrity: sha512-pWIG9a/x1ky8gXKRhPH1OPKpHFoMN1ISLbJ+O+gPXQHIAKhNd5I28RlWf7q576hAOQA9JZTlo3p/M2uyLzJmmw==, + } + engines: { node: ">= 10" } + dev: false + /@rushstack/eslint-patch/1.2.0: resolution: { @@ -2050,6 +2060,18 @@ packages: } 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 + /@swc/helpers/0.4.11: resolution: { @@ -2284,6 +2306,13 @@ packages: integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==, } + /@types/yoga-layout/1.9.2: + resolution: + { + integrity: sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==, + } + dev: false + /@typescript-eslint/parser/5.44.0_qugx7qdu5zevzvxaiqyxfiwquq: resolution: { @@ -2372,6 +2401,18 @@ packages: react: 18.2.0 dev: false + /@vercel/og/0.0.21: + resolution: + { + integrity: sha512-WgMahG5c8UM7xtn/mT+ljUpDMSDnSlu8AXL52JtTwxb1odrd0GWqZg2N1X38wulPj+sCccNpDdFmmAuw0GLc+Q==, + } + engines: { node: ">=16" } + dependencies: + "@resvg/resvg-wasm": 2.0.0-alpha.4 + satori: 0.0.44 + yoga-wasm-web: 0.1.2 + dev: false + /JSONStream/1.3.5: resolution: { @@ -2860,6 +2901,13 @@ packages: engines: { node: ">=6" } dev: true + /camelize/1.0.1: + resolution: + { + integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==, + } + dev: false + /caniuse-lite/1.0.30001434: resolution: { @@ -3238,6 +3286,39 @@ packages: which: 2.0.2 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-to-react-native/3.0.0: + resolution: + { + integrity: sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==, + } + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + dev: false + /cssesc/3.0.0: resolution: { @@ -3518,6 +3599,13 @@ packages: } dev: true + /emoji-regex/10.2.1: + resolution: + { + integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==, + } + dev: false + /emoji-regex/8.0.0: resolution: { @@ -4452,6 +4540,13 @@ packages: web-streams-polyfill: 3.2.1 dev: false + /fflate/0.7.4: + resolution: + { + integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==, + } + dev: false + /file-entry-cache/6.0.1: resolution: { @@ -8241,6 +8336,22 @@ packages: } dev: false + /satori/0.0.44: + resolution: + { + integrity: sha512-WKUxXC2qeyno6J3ucwwLozPL6j1HXOZiN5wIUf7iqAhlx1RUC/6ePIKHi7iPc3Cy6DYuZcJriZXxXkSdo2FQHg==, + } + 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.0.0 + emoji-regex: 10.2.1 + postcss-value-parser: 4.2.0 + yoga-layout-prebuilt: 1.10.0 + dev: false + /scheduler/0.23.0: resolution: { @@ -8519,6 +8630,13 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string.prototype.codepointat/0.2.1: + resolution: + { + integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==, + } + dev: false + /string.prototype.matchall/4.0.8: resolution: { @@ -9540,6 +9658,23 @@ packages: engines: { node: ">=10" } dev: true + /yoga-layout-prebuilt/1.10.0: + resolution: + { + integrity: sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==, + } + engines: { node: ">=8" } + dependencies: + "@types/yoga-layout": 1.9.2 + dev: false + + /yoga-wasm-web/0.1.2: + resolution: + { + integrity: sha512-8SkgawHcA0RUbMrnhxbaQkZDBi8rMed8pQHixkFF9w32zGhAwZ9/cOHWlpYfr6RCx42Yp3siV45/jPEkJxsk6w==, + } + dev: false + /zod/3.19.1: resolution: {