Skip to content

Commit

Permalink
feat: dynamic OG images for docs t3-oss#986 (t3-oss#1291)
Browse files Browse the repository at this point in the history
* Prepare for OG image generation

* Setup basic og image generation

Need to tweak it to make it look nicer, doing this for testing quick

* Dont optimize @resvg/resvg-js

* Include protocol in URL for OG

* Use OG route for OG images in docs

* Use vercel URL for og images in meta

* Cache OG responses

* Changes to fetching fonts

* Final OG image design

* Make util for site url

* Setup env for satori debug

* -p doesnt include untracked files ._.

* Delete unneeded assets

* Make file name consistent

* design 2

* Design 3

* Setup OG to use reading time + path route

* Fix url on og image

* Fix reading time

* Use Astro site hostname

* Add fonts for other languages (ar, zh-hans)

* Fix linting issues

* fix env debug mode

* Add support for RTL languages in OG images

* Remove unneeded not null assertion

* Remove reading time

After giving it some more thought, having a reading time doesn't make sense for docs, a blog sure but not docs.

* Fix overflowing issue with long titles

* Make title font smaller for larger text

* Remove unused dep

* Remove unused Frontmatter prop

* Add reference to original place for og fonts code

* Format astro config

what actually changed I will never know.

* Fix broken lock file

* Actually fix lock file?

* Remove unused reading time parameter

* Use existing rtl language map

---------

Co-authored-by: Christopher Ehrlich <ehrlich.christopher@gmail.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
  • Loading branch information
3 people authored Apr 24, 2023
1 parent 544f6c4 commit e828a29
Show file tree
Hide file tree
Showing 10 changed files with 7,290 additions and 2,864 deletions.
9,881 changes: 7,020 additions & 2,861 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions www/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ PUBLIC_GITHUB_TOKEN=
# Twitter (API v2)- for grabbing Twitter card data
# https://developer.twitter.com/en/docs/authentication/overview
TWITTER_BEARER_TOKEN=

# For enabling debug mode in satori (for OG images)
DEBUG_OG=
12 changes: 10 additions & 2 deletions www/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import image from "@astrojs/image";
import mdx from "@astrojs/mdx";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import { defineConfig } from "astro/config";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeExternalLinks from "rehype-external-links";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import sitemap from "@astrojs/sitemap";
import vercel from "@astrojs/vercel/serverless";
import remarkCodeTitles from "remark-code-titles";

/** @link https://astro.build/config */
export default defineConfig({
site: `https://create.t3.gg/`,
output: "server",
adapter: vercel(),
markdown: {
remarkPlugins: [remarkCodeTitles],
rehypePlugins: [
Expand Down Expand Up @@ -50,4 +53,9 @@ export default defineConfig({
sitemap(),
mdx(),
],
vite: {
optimizeDeps: {
exclude: ["@resvg/resvg-js"],
},
},
});
4 changes: 4 additions & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,22 @@
"@astrojs/image": "^0.15.1",
"@astrojs/mdx": "^0.17.2",
"@astrojs/sitemap": "^1.2.1",
"@astrojs/vercel": "^3.2.1",
"@docsearch/css": "^3.3.3",
"@docsearch/react": "^3.3.3",
"@fontsource/inter": "^4.5.15",
"@fontsource/jetbrains-mono": "^4.5.12",
"@headlessui/react": "^1.7.13",
"@resvg/resvg-js": "^2.4.1",
"@vercel/analytics": "^0.1.11",
"clsx": "^1.2.1",
"embla-carousel": "^7.1.0",
"embla-carousel-autoplay": "^7.1.0",
"satori": "^0.4.4",
"sharp": "^0.31.3",
"tailwind-scrollbar": "^2.1.0",
"treeify": "^1.1.0",
"unist-util-visit": "^4.1.2",
"zod": "^3.21.4"
},
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions www/public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion www/src/components/headSeo.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import { SITE, OPEN_GRAPH, type Frontmatter } from "../config";
import { SITE_URL } from "../utils/siteUrl";
export interface Props {
frontmatter?: Frontmatter;
Expand All @@ -14,7 +15,21 @@ const formattedContentTitle = frontmatter
? `${frontmatter.title} 🚀 ${SITE.title}`
: SITE.title;
const imageSrc = frontmatter?.image?.src ?? OPEN_GRAPH.image.src;
const ogTitle = frontmatter ? frontmatter.title : SITE.title;
const ogDescription = frontmatter ? frontmatter.description : SITE.description;
const ogImageData = {
title: ogTitle,
description: ogDescription,
pagePath: Astro.url.pathname,
};
const imageSrc =
frontmatter?.image?.src ??
`${SITE_URL}/og?${Object.entries(ogImageData)
.map(([key, value]) => `${key}=${value}`)
.join("&")}`;
const imageUrl = new URL(imageSrc, Astro.url.origin);
const imageAlt = frontmatter?.image?.alt ?? OPEN_GRAPH.image.alt;
Expand Down
113 changes: 113 additions & 0 deletions www/src/components/openGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
type OpenGraphProps = {
title: string;
description: string;
imageBase: string;
pageUrl: string;
rtl: boolean;
};

export default function OpenGraph({
title,
description,
imageBase,
pageUrl,
rtl,
}: OpenGraphProps) {
return (
<div
style={{
display: "flex",
width: "1200px",
height: "630px",
justifyContent: "center",
alignItems: "center",
gap: "3rem",
flexDirection: rtl ? "row-reverse" : "row",
background:
"linear-gradient(180deg, rgba(48,1,113,1) 0%, rgba(17,24,39,1) 100%)",
}}
>
<img
src={`${imageBase}/images/background-pattern.svg`}
style={{
position: "absolute",
width: "1200px",
height: "1200px",
opacity: 0.15,
}}
/>
<Logo color={"#F5F5F5"} />
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "1.2rem",
}}
>
<h1
style={{
textAlign: "center",
fontSize: title.length > 15 ? "70px" : "90px",
lineHeight: "5rem",
fontWeight: 700,
color: "#fff",
maxWidth: "700px",
}}
>
{title}
</h1>
<h2
style={{
color: "#F5F5F5",
fontSize: "40px",
fontWeight: 400,
maxWidth: "700px",
textAlign: "center",
wordBreak: "break-word",
}}
>
{description}
</h2>
</div>
<h3
style={{
fontSize: "40px",
color: "#c3b4fc",
fontWeight: 400,
position: "absolute",
bottom: "20px",
}}
>
{pageUrl}
</h3>
</div>
);
}

const Logo = ({ color }: { color: string }) => (
<svg
width="268"
height="203"
viewBox="0 0 268 203"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M152.981 29.4786L180.491 0.918945L0.850377 0.918945V29.4786H152.981Z"
fill={color}
/>
<path
d="M159.664 101.527L257.947 1.29251L218.808 1.29228L137.874 83.0602L159.664 101.527Z"
fill={color}
/>
<path
d="M155.638 131.857L132.692 154.803L135.035 160.378C145.494 185.262 170.104 202.762 198.823 202.762C237.023 202.762 267.99 171.795 267.99 133.595C267.99 108.277 254.171 86.3783 234.102 74.3543L228.039 70.7214L207.028 92.0006L217.746 97.6588C230.659 104.475 239.427 118.019 239.427 133.595C239.427 156.021 221.248 174.2 198.823 174.2C180.714 174.2 165.352 162.339 160.126 145.94L155.638 131.857Z"
fill={color}
/>
<path
d="M98.4934 197.078L98.4934 52.2128H69.9338L69.9338 197.078H98.4934Z"
fill={color}
/>
</svg>
);
72 changes: 72 additions & 0 deletions www/src/pages/og.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import satori from "satori";
import OpenGraph from "../components/openGraph";
import { type APIRoute } from "astro";
import { Resvg } from "@resvg/resvg-js";
import { getFont } from "../utils/ogFont";
import { SITE_URL } from "../utils/siteUrl";
import { SITE } from "../config";
import { getIsRtlFromLangCode, getLanguageFromURL } from "../languages";

const removeEndingSlash = (str: string) => str.replace(/\/$/, "");

export const get: APIRoute = async (request) => {
const params = request.url.searchParams;
const title = params.get("title") ?? SITE.title;
const description = params.get("description") ?? SITE.description;
const pagePath = params.get("pagePath") ?? "";

// Used for most languages
const inter = await getFont({
family: "Inter",
weights: [400, 700] as const,
});

// Used for arabic text
const bonaNova = await getFont({
family: "Bona Nova",
weights: [400, 700] as const,
});

// Used for chinese
const notoSans = await getFont({
family: "Noto Sans SC",
weights: [400, 700] as const,
});

const hostname = request.site?.hostname.replace(/^https?:\/\//, "");
const pageLang = getLanguageFromURL(pagePath);

const svg = await satori(
OpenGraph({
title,
description,
imageBase: SITE_URL,
pageUrl: `${hostname}${removeEndingSlash(pagePath)}`,
rtl: getIsRtlFromLangCode(pageLang),
}),
{
width: 1200,
height: 630,
fonts: [
{ name: "Inter", data: inter[400], weight: 400 },
{ name: "Inter", data: inter[700], weight: 700 },
{ name: "Noto Sans SC", data: notoSans[400], weight: 400 },
{ name: "Noto Sans SC", data: notoSans[700], weight: 700 },
{ name: "Bona Nova", data: bonaNova[400], weight: 400 },
{ name: "Bona Nova", data: bonaNova[700], weight: 700 },
],
debug: import.meta.env.DEBUG_OG === "true" ?? false,
},
);

const resvg = new Resvg(svg, {});
const pngData = resvg.render();
const pngBuffer = pngData.asPng();

return new Response(pngBuffer, {
headers: {
"Content-Type": "image/png",
"cache-control": "public, max-age=31536000, immutable",
},
});
};
43 changes: 43 additions & 0 deletions www/src/utils/ogFont.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/** https://github.com/juliusmarminge/jumr.dev/blob/main/app/og-image/get-fonts.ts */

export async function getFont<TWeights extends readonly number[]>({
family,
weights,
text,
}: {
family: string;
weights: TWeights;
text?: string;
}): Promise<Record<TWeights[number], ArrayBuffer>> {
const API = `https://fonts.googleapis.com/css2?family=${family}:wght@${weights.join(
";",
)}${text ? `&text=${encodeURIComponent(text)}` : ""}`;

const css = await (
await fetch(API, {
headers: {
// Make sure it returns TTF.
"User-Agent":
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
},
})
).text();

const fonts = css
.split("@font-face {")
.splice(1)
.map((font) => {
const u = font.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/);
const w = font.match(/font-weight: (\d+)/);
return u?.[1] && w?.[1] ? { url: u[1], weight: parseInt(w[1]) } : null;
})
.filter(
(font): font is { url: string; weight: TWeights[number] } => !!font,
);

const promises = fonts.map(async (font) => {
const res = await fetch(font.url);
return [font.weight, await res.arrayBuffer()];
});
return Object.fromEntries(await Promise.all(promises));
}
3 changes: 3 additions & 0 deletions www/src/utils/siteUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const SITE_URL = import.meta.env.VERCEL_URL
? `https://${import.meta.env.VERCEL_URL}`
: "http://localhost:3000";

0 comments on commit e828a29

Please sign in to comment.