From a9bb559568917540f3274480ce10564f925f9d85 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:56:46 +0200 Subject: [PATCH 1/6] fix: avoid opengraph image limit --- .../blog/[post]/opengraph/route.tsx | 77 +++++++++++++++++++ .../src/app/(marketing)/blog/[post]/page.tsx | 8 +- 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx new file mode 100644 index 000000000..906ee18cd --- /dev/null +++ b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx @@ -0,0 +1,77 @@ +import { ImageResponse } from 'next/og'; +import { NextResponse } from 'next/server'; + +import { allBlogPosts } from 'contentlayer/generated'; + +export const runtime = 'edge'; + +const contentType = 'image/png'; + +const IMAGE_SIZE = { + width: 1200, + height: 630, +}; + +type BlogPostOpenGraphImageProps = { + params: { post: string }; +}; + +export async function GET(_request: Request, { params }: BlogPostOpenGraphImageProps) { + const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); + + if (!blogPost) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } + + // The long urls are needed for a compiler optimisation on the Next.js side, lifting this up + // to a constant will break og image generation. + const [interBold, interRegular, backgroundImage, logoImage] = await Promise.all([ + fetch(new URL('@documenso/assets/fonts/inter-bold.ttf', import.meta.url)).then(async (res) => + res.arrayBuffer(), + ), + fetch(new URL('@documenso/assets/fonts/inter-regular.ttf', import.meta.url)).then(async (res) => + res.arrayBuffer(), + ), + fetch(new URL('@documenso/assets/images/background-blog-og.png', import.meta.url)).then( + async (res) => res.arrayBuffer(), + ), + fetch(new URL('@documenso/assets/logo.png', import.meta.url)).then(async (res) => + res.arrayBuffer(), + ), + ]); + + return new ImageResponse( + ( +
+ {/* @ts-expect-error Lack of typing from ImageResponse */} + og-background + + {/* @ts-expect-error Lack of typing from ImageResponse */} + logo + +

+ {blogPost.title} +

+ +

Written by {blogPost.authorName}

+
+ ), + { + ...IMAGE_SIZE, + fonts: [ + { + name: 'Inter', + data: interRegular, + style: 'normal', + weight: 400, + }, + { + name: 'Inter', + data: interBold, + style: 'normal', + weight: 700, + }, + ], + }, + ); +} diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx index bd5fdb2da..fc65d9772 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx @@ -25,6 +25,12 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => { absolute: `${blogPost.title} - Documenso Blog`, }, description: blogPost.description, + openGraph: { + images: [`${blogPost.href}/opengraph`], + }, + twitter: { + images: [`${blogPost.href}/opengraph`], + }, }; }; @@ -94,7 +100,7 @@ export default function BlogPostPage({ params }: { params: { post: string } }) { - {post.cta && } + {post.cta && } ); } From d6c8a3d32c5939536f8c87e4b6938f03d35744f8 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 14 Mar 2024 09:20:01 +0000 Subject: [PATCH 2/6] fix: what happens if we use a dynamic import? --- .../src/app/(marketing)/blog/[post]/opengraph/route.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx index 906ee18cd..6f16b5092 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx @@ -1,12 +1,8 @@ import { ImageResponse } from 'next/og'; import { NextResponse } from 'next/server'; -import { allBlogPosts } from 'contentlayer/generated'; - export const runtime = 'edge'; -const contentType = 'image/png'; - const IMAGE_SIZE = { width: 1200, height: 630, @@ -17,6 +13,8 @@ type BlogPostOpenGraphImageProps = { }; export async function GET(_request: Request, { params }: BlogPostOpenGraphImageProps) { + const { allBlogPosts } = await import('contentlayer/generated'); + const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); if (!blogPost) { From 4926b6de509b7b5942a16238030507faf7098f74 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 14 Mar 2024 09:40:26 +0000 Subject: [PATCH 3/6] fix: boring sign/verify approach --- .../blog/[post]/opengraph/route.tsx | 22 +++++++++++-------- .../src/app/(marketing)/blog/[post]/page.tsx | 18 +++++++++++++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx index 6f16b5092..f17a7931a 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx @@ -1,6 +1,8 @@ import { ImageResponse } from 'next/og'; import { NextResponse } from 'next/server'; +import { verify } from '@documenso/lib/server-only/crypto/verify'; + export const runtime = 'edge'; const IMAGE_SIZE = { @@ -8,16 +10,18 @@ const IMAGE_SIZE = { height: 630, }; -type BlogPostOpenGraphImageProps = { - params: { post: string }; -}; +export async function GET(_request: Request) { + const url = new URL(_request.url); -export async function GET(_request: Request, { params }: BlogPostOpenGraphImageProps) { - const { allBlogPosts } = await import('contentlayer/generated'); + const signature = url.searchParams.get('sig'); + const title = url.searchParams.get('title'); + const author = url.searchParams.get('author'); - const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); + if (!title || !author || !signature) { + return NextResponse.json({ error: 'Not found' }, { status: 404 }); + } - if (!blogPost) { + if (!verify({ title, author }, signature)) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } @@ -48,10 +52,10 @@ export async function GET(_request: Request, { params }: BlogPostOpenGraphImageP logo

- {blogPost.title} + {title}

-

Written by {blogPost.authorName}

+

Written by {author}

), { diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx index fc65d9772..d8ef587c4 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx @@ -7,6 +7,8 @@ import { ChevronLeft } from 'lucide-react'; import type { MDXComponents } from 'mdx/types'; import { useMDXComponent } from 'next-contentlayer/hooks'; +import { sign } from '@documenso/lib/server-only/crypto/sign'; + import { CallToAction } from '~/components/(marketing)/call-to-action'; export const dynamic = 'force-dynamic'; @@ -20,16 +22,28 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => { }; } + const signature = sign({ + title: blogPost.title, + author: blogPost.authorName, + }); + + // Use the url constructor to ensure that things are escaped as they should be + const openGraphImageUrl = new URL(`${blogPost.href}/opengraph`); + + openGraphImageUrl.searchParams.set('title', blogPost.title); + openGraphImageUrl.searchParams.set('author', blogPost.authorName); + openGraphImageUrl.searchParams.set('sig', signature); + return { title: { absolute: `${blogPost.title} - Documenso Blog`, }, description: blogPost.description, openGraph: { - images: [`${blogPost.href}/opengraph`], + images: [openGraphImageUrl.toString()], }, twitter: { - images: [`${blogPost.href}/opengraph`], + images: [openGraphImageUrl.toString()], }, }; }; From f5967e28c3fa751b54e1c9ad79bbb04ee52641b9 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 14 Mar 2024 10:02:09 +0000 Subject: [PATCH 4/6] fix: without protection? --- .../blog/[post]/opengraph-image.tsx | 76 ------------------- .../blog/[post]/opengraph/route.tsx | 9 +-- 2 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 apps/marketing/src/app/(marketing)/blog/[post]/opengraph-image.tsx diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph-image.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph-image.tsx deleted file mode 100644 index 4c01967d2..000000000 --- a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph-image.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { ImageResponse } from 'next/og'; - -import { allBlogPosts } from 'contentlayer/generated'; - -export const runtime = 'edge'; - -export const contentType = 'image/png'; - -export const IMAGE_SIZE = { - width: 1200, - height: 630, -}; - -type BlogPostOpenGraphImageProps = { - params: { post: string }; -}; - -export default async function BlogPostOpenGraphImage({ params }: BlogPostOpenGraphImageProps) { - const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`); - - if (!blogPost) { - return null; - } - - // The long urls are needed for a compiler optimisation on the Next.js side, lifting this up - // to a constant will break og image generation. - const [interBold, interRegular, backgroundImage, logoImage] = await Promise.all([ - fetch(new URL('@documenso/assets/fonts/inter-bold.ttf', import.meta.url)).then(async (res) => - res.arrayBuffer(), - ), - fetch(new URL('@documenso/assets/fonts/inter-regular.ttf', import.meta.url)).then(async (res) => - res.arrayBuffer(), - ), - fetch(new URL('@documenso/assets/images/background-blog-og.png', import.meta.url)).then( - async (res) => res.arrayBuffer(), - ), - fetch(new URL('@documenso/assets/logo.png', import.meta.url)).then(async (res) => - res.arrayBuffer(), - ), - ]); - - return new ImageResponse( - ( -
- {/* @ts-expect-error Lack of typing from ImageResponse */} - og-background - - {/* @ts-expect-error Lack of typing from ImageResponse */} - logo - -

- {blogPost.title} -

- -

Written by {blogPost.authorName}

-
- ), - { - ...IMAGE_SIZE, - fonts: [ - { - name: 'Inter', - data: interRegular, - style: 'normal', - weight: 400, - }, - { - name: 'Inter', - data: interBold, - style: 'normal', - weight: 700, - }, - ], - }, - ); -} diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx index f17a7931a..70233bbdd 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/opengraph/route.tsx @@ -1,8 +1,6 @@ import { ImageResponse } from 'next/og'; import { NextResponse } from 'next/server'; -import { verify } from '@documenso/lib/server-only/crypto/verify'; - export const runtime = 'edge'; const IMAGE_SIZE = { @@ -13,15 +11,10 @@ const IMAGE_SIZE = { export async function GET(_request: Request) { const url = new URL(_request.url); - const signature = url.searchParams.get('sig'); const title = url.searchParams.get('title'); const author = url.searchParams.get('author'); - if (!title || !author || !signature) { - return NextResponse.json({ error: 'Not found' }, { status: 404 }); - } - - if (!verify({ title, author }, signature)) { + if (!title || !author) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } From 0db2e6643dd8678283eb3b1ab3ab499bfe62a0fc Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 14 Mar 2024 10:39:48 +0000 Subject: [PATCH 5/6] fix: final final v2 --- .../src/app/(marketing)/blog/[post]/page.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx index d8ef587c4..b0d59edc1 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx @@ -28,11 +28,11 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => { }); // Use the url constructor to ensure that things are escaped as they should be - const openGraphImageUrl = new URL(`${blogPost.href}/opengraph`); - - openGraphImageUrl.searchParams.set('title', blogPost.title); - openGraphImageUrl.searchParams.set('author', blogPost.authorName); - openGraphImageUrl.searchParams.set('sig', signature); + const searchParams = new URLSearchParams({ + title: blogPost.title, + author: blogPost.authorName, + sig: signature, + }); return { title: { @@ -40,10 +40,10 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => { }, description: blogPost.description, openGraph: { - images: [openGraphImageUrl.toString()], + images: [`${blogPost.href}/opengraph?${searchParams.toString()}`], }, twitter: { - images: [openGraphImageUrl.toString()], + images: [`${blogPost.href}/opengraph?${searchParams.toString()}`], }, }; }; From 524a7918d55754a7c26e107f268db298cd7a3eb3 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 14 Mar 2024 10:41:59 +0000 Subject: [PATCH 6/6] fix: toss the signature --- apps/marketing/src/app/(marketing)/blog/[post]/page.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx index b0d59edc1..3e50f8305 100644 --- a/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx +++ b/apps/marketing/src/app/(marketing)/blog/[post]/page.tsx @@ -7,8 +7,6 @@ import { ChevronLeft } from 'lucide-react'; import type { MDXComponents } from 'mdx/types'; import { useMDXComponent } from 'next-contentlayer/hooks'; -import { sign } from '@documenso/lib/server-only/crypto/sign'; - import { CallToAction } from '~/components/(marketing)/call-to-action'; export const dynamic = 'force-dynamic'; @@ -22,16 +20,10 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => { }; } - const signature = sign({ - title: blogPost.title, - author: blogPost.authorName, - }); - // Use the url constructor to ensure that things are escaped as they should be const searchParams = new URLSearchParams({ title: blogPost.title, author: blogPost.authorName, - sig: signature, }); return {