Skip to content

Commit

Permalink
@vercel/og cover image generation for blog posts and home
Browse files Browse the repository at this point in the history
  • Loading branch information
flexdinesh committed Nov 1, 2022
1 parent 8947a5b commit e6d15c9
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 2 deletions.
28 changes: 28 additions & 0 deletions docs/lib/og-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
let baseUrl = 'http:/localhost:8000';
if (process.env.NEXT_PUBLIC_VERCEL_ENV === 'production') {
baseUrl = 'https://keystonejs.com';
} else if (process.env.NEXT_PUBLIC_VERCEL_URL) {
baseUrl = `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
}

export const getOgAbsoluteUrl = ({
title,
description,
type,
}: {
title: string;
description?: string;
type?: string;
}) => {
const ogUrl = new URL(`${baseUrl}/api/hero-image`);

ogUrl.searchParams.append('title', title);
if (typeof description === 'string') {
ogUrl.searchParams.append('description', description);
}
if (typeof type === 'string') {
ogUrl.searchParams.append('type', type);
}

return ogUrl.href;
};
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@types/mdx-js__react": "^1.5.5",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4",
"@vercel/og": "^0.0.20",
"classnames": "^2.3.1",
"clipboard-copy": "^4.0.1",
"date-fns": "^2.26.0",
Expand Down
149 changes: 149 additions & 0 deletions docs/pages/api/hero-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react';
import { ImageResponse } from '@vercel/og';
import type { NextRequest } from 'next/server';

export const config = {
runtime: 'experimental-edge',
};

const HeroImage = ({
title,
description,
type,
}: {
title: string;
description?: string;
type?: string;
}) => {
let titleFontSize = 96;
if (title.length > 20) {
titleFontSize = 80;
} else if (title.length > 30) {
titleFontSize = 72;
} else if (title.length > 100) {
titleFontSize = 60;
}

const shortenedDescription =
typeof description === 'string' && description?.length > 110
? description?.substring(0, 110) + '...'
: description || '';
return (
<div
style={{
display: 'flex',
backgroundColor: 'white',
backgroundImage: 'linear-gradient(135deg, #1476FF, #00ABDA)',
height: '100%',
width: '100%',
}}
>
<div
style={{
position: 'relative',
padding: '40px 80px',
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<div
style={{
letterSpacing: -2,
color: '#FFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
}}
>
{type ? (
<div
style={{
fontSize: 32,
paddingBottom: 8,
alignSelf: 'flex-start',
}}
>
{type}
</div>
) : null}
<div
style={{
fontSize: titleFontSize,
fontWeight: 700,
paddingTop: 16,
paddingBottom: 64,
borderTop: type ? '1px solid white' : '1px solid transparent',
}}
>
{title}
</div>
{shortenedDescription ? (
<div style={{ fontSize: 40, fontWeight: 400, lineHeight: 1.4 }}>
{shortenedDescription}
</div>
) : null}
</div>
</div>
</div>
);
};
// Make sure the font exists in the specified path:
const interBold = fetch(new URL('../../public/font/Inter-Bold.ttf', import.meta.url)).then(res =>
res.arrayBuffer()
);

const interRegular = fetch(new URL('../../public/font/Inter-Regular.ttf', import.meta.url)).then(
res => res.arrayBuffer()
);

// vercel API route that generates the OG image
export default async function handler(req: NextRequest) {
const interBoldData = await interBold;
const interRegularData = await interRegular;

try {
const { searchParams } = new URL(req.url);
const title = searchParams.has('title') ? searchParams.get('title') || '' : '';
const description = searchParams.get('description') || undefined;
const type = searchParams.get('type') || undefined;

if (title?.length > 100 || (typeof description === 'string' && description?.length > 300)) {
return new Response(
JSON.stringify({
code: 'INVALID_PARAMS',
message: 'Param title/description too long',
}),
{ status: 400 }
);
}

return new ImageResponse(<HeroImage title={title} description={description} type={type} />, {
width: 1200,
height: 630,
emoji: 'twemoji',
fonts: [
{
name: 'Inter',
data: interBoldData,
style: 'normal',
weight: 700,
},
{
name: 'Inter',
data: interRegularData,
style: 'normal',
weight: 400,
},
],
});
} catch (e: any) {
return new Response(`Failed to generate hero image`, {
status: 500,
});
}
}
13 changes: 12 additions & 1 deletion docs/pages/blog/[post].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { extractHeadings, Markdoc } from '../../components/Markdoc';
import { BlogPage } from '../../components/Page';
import { Heading } from '../../components/docs/Heading';
import { Type } from '../../components/primitives/Type';
import { getOgAbsoluteUrl } from '../../lib/og-util';

export default function Page(props: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter();
Expand All @@ -28,10 +29,20 @@ export default function Page(props: InferGetStaticPropsType<typeof getStaticProp
const publishedDate = props.publishDate;
const parsedDate = parse(publishedDate, 'yyyy-M-d', new Date());
const formattedDateStr = format(parsedDate, 'MMMM do, yyyy');

let ogImageUrl = props.metaImageUrl;
if (!ogImageUrl) {
ogImageUrl = getOgAbsoluteUrl({
title: props.title,
description: props.description,
type: 'blog',
});
}

return (
<BlogPage
headings={headings}
ogImage={props.metaImageUrl}
ogImage={ogImageUrl}
title={props.title}
description={props.description}
editPath={`docs/pages/docs/${router.query.post}.md`}
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/blog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Type } from '../../components/primitives/Type';
import { Highlight } from '../../components/primitives/Highlight';
import { useMediaQuery } from '../../lib/media';
import { BlogFrontmatter, extractBlogFrontmatter } from '../../markdoc';
import { getOgAbsoluteUrl } from '../../lib/og-util';

const today = new Date();
export default function Docs(props: InferGetStaticPropsType<typeof getStaticProps>) {
Expand All @@ -35,10 +36,16 @@ export default function Docs(props: InferGetStaticPropsType<typeof getStaticProp
})
.sort((a, b) => (a.parsedDate < b.parsedDate ? 1 : -1));

const ogImageUrl = getOgAbsoluteUrl({
title: 'The Keystone Blog',
description: 'Latest news and announcements from the Keystone team.',
});

return (
<Page
title={'The Keystone Blog'}
description={'Blog posts from the team maintaining KeystoneJS.'}
ogImage={ogImageUrl}
>
<MWrapper css={{ marginTop: 0 }}>
<section
Expand Down
Binary file added docs/public/font/Inter-Bold.ttf
Binary file not shown.
Binary file added docs/public/font/Inter-Regular.ttf
Binary file not shown.
Loading

0 comments on commit e6d15c9

Please sign in to comment.