Skip to content

Commit

Permalink
feat: add og image gen (#191)
Browse files Browse the repository at this point in the history
* feat: add og image gen

* feat: add og imgae gen

* chore: self review
  • Loading branch information
tom-bywild authored Oct 29, 2024
1 parent 5b8ed82 commit 610b345
Show file tree
Hide file tree
Showing 17 changed files with 791 additions and 113 deletions.
5 changes: 5 additions & 0 deletions web/app/modules/article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mergeMeta } from "./meta";
import { InfoPill } from "./info-pill";
import clsx from "clsx";
import { InfoCard } from "./info-card";
import { BASE_URL } from "./app";

export type NewsArticleHandle = {
slug: string;
Expand All @@ -20,6 +21,10 @@ export const composeArticleMeta = mergeMeta(({ matches }) => {
return [
{ title: `${article?.title} | CRAN/E` },
{ name: "description", content: article?.subline },
{ property: "og:title", content: `${article?.title} | CRAN/E` },
{ property: "og:url", content: `${BASE_URL}/press/news/${article?.slug}` },
{ property: "og:description", content: article?.subline },
{ property: "og:image", content: `${BASE_URL}/press/news/og` },
];
});

Expand Down
2 changes: 1 addition & 1 deletion web/app/modules/contact-pill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function ContactPill(props: Props) {
className="text-gold-1 dark:text-gold-2"
/>
}
className="border-transparent bg-gold-10 text-gold-1 dark:bg-gold-11 dark:text-gold-2"
className="border-transparent text-gold-1 dark:bg-gold-12"
>
Maintainer
</InfoPill>
Expand Down
6 changes: 2 additions & 4 deletions web/app/modules/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ExternalLink } from "./external-link";
import { cva, VariantProps } from "cva";
import { ReactNode } from "react";
import clsx from "clsx";
import { RiGithubLine } from "@remixicon/react";

const BASE_ITEMS: Array<{ label: string; href: string }> = [
{ label: "About", href: "/about" },
Expand Down Expand Up @@ -53,12 +52,11 @@ export function Footer(props: Props) {
href="https://github.com/flaming-codes/crane-app"
className="underline-offset-4 hover:brightness-75"
>
<RiGithubLine size={16} />
<span className="sr-only">Github</span>
<span>Github</span>
</ExternalLink>
</li>
{version && (
<li className="text-gray-dim font-mono text-xs opacity-70 hover:brightness-75">
<li className="text-gray-dim font-mono text-xs opacity-90 hover:brightness-75">
<ExternalLink href="https://github.com/flaming-codes/crane-app">
v{version}
</ExternalLink>
Expand Down
139 changes: 139 additions & 0 deletions web/app/modules/meta-og-image.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// ./app/utils/createOGImage.server.tsx

import { Resvg } from "@resvg/resvg-js";
import type { SatoriOptions } from "satori";
import satori from "satori";

const OG_IMAGE_HEIGHT = 630;
const OG_IMAGE_WIDTH = 1200;

function getRegularSans(baseUrl: string) {
return fetch(new URL(`${baseUrl}/fonts/Inter-Regular.ttf`)).then(
async (res) => {
return res.arrayBuffer();
},
);
}

async function getBaseOptions(
requestUrl: string,
partial: Partial<SatoriOptions> = {},
): Promise<SatoriOptions> {
const fontSansData = await getRegularSans(requestUrl);

return {
width: OG_IMAGE_WIDTH,
height: OG_IMAGE_HEIGHT,
fonts: [
{
name: "Inter",
data: fontSansData,
weight: 400,
style: "normal",
},
],
...partial,
};
}

export async function composeAuthorOGImage(params: {
name: string;
requestUrl: string;
}) {
const { name, requestUrl } = params;

// Design the image and generate an SVG with "satori"
const svg = await satori(
<div
style={{
width: OG_IMAGE_WIDTH,
height: OG_IMAGE_HEIGHT,
background: "linear-gradient(to bottom left, #208368, #000)",
color: "white",
fontFamily: "Inter",
fontSize: 80,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 20,
textAlign: "center",
padding: 40,
}}
>
{name}
</div>,
await getBaseOptions(requestUrl),
);

return new Resvg(svg).render().asPng();
}

export async function composePackageOGImage(params: {
name: string;
requestUrl: string;
}) {
const { name, requestUrl } = params;

// Design the image and generate an SVG with "satori"
const svg = await satori(
<div
style={{
width: OG_IMAGE_WIDTH,
height: OG_IMAGE_HEIGHT,
background: "linear-gradient(to bottom left, #5151cd, #000)",
color: "white",
fontFamily: "Inter",
fontSize: 80,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 20,
textAlign: "center",
padding: 40,
}}
>
{name}
</div>,
await getBaseOptions(requestUrl),
);

return new Resvg(svg).render().asPng();
}

export async function composeNewsArticleOGImage(params: {
headline: string;
subline?: string;
requestUrl: string;
}) {
const { headline, subline, requestUrl } = params;

// Design the image and generate an SVG with "satori"
const svg = await satori(
<div
style={{
width: OG_IMAGE_WIDTH,
height: OG_IMAGE_HEIGHT,
background: "linear-gradient(to top left, #953ea3, #2f265f,#000 )",
color: "white",
fontFamily: "Inter",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 20,
textAlign: "center",
padding: 40,
}}
>
<span style={{ fontSize: 80 }}>{headline}</span>
{subline ? (
<span style={{ fontSize: 60, opacity: 80 }}>{subline}</span>
) : null}
</div>,
await getBaseOptions(requestUrl),
);

return new Resvg(svg).render().asPng();
}
2 changes: 2 additions & 0 deletions web/app/modules/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const mergeMeta = (
);
if (index !== -1) {
mergedMeta.splice(index, 1, override);
} else {
mergedMeta.push(override);
}
}

Expand Down
6 changes: 5 additions & 1 deletion web/app/modules/svg.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { CSSProperties } from "react";

type Props = {
className?: string;
style?: CSSProperties;
};

export function SineLogo({ className }: Props) {
export function SineLogo({ className, style }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 453.801 87.3"
className={className}
style={style}
>
<path
stroke="currentColor"
Expand Down
37 changes: 37 additions & 0 deletions web/app/routes/_page.author.$authorId.og.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { LoaderFunctionArgs } from "@remix-run/node";
import { composeAuthorOGImage } from "../modules/meta-og-image.server";
import { ENV } from "../data/env";
import { addDays, getSeconds } from "date-fns";
import { authorSlugSchema } from "../data/author.shape";

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { origin } = new URL(request.url);
const { authorId } = params;

const parsedId = authorSlugSchema.safeParse(authorId);
if (parsedId.error) {
throw new Response(null, {
status: 400,
statusText: "Valid author ID is required",
});
}

const png = await composeAuthorOGImage({
name: encodeURIComponent(parsedId.data),
requestUrl: origin,
});

// Respond with the PNG buffer
return new Response(png, {
status: 200,
headers: {
// Tell the browser the response is an image
"Content-Type": "image/png",
// Tip: You might want to heavily cache the response in production
"cache-control":
ENV.NODE_ENV === "production"
? `public, immutable, no-transform, max-age=${getSeconds(addDays(new Date(), 365))}`
: "no-cache",
},
});
};
20 changes: 12 additions & 8 deletions web/app/routes/_page.author.$authorId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,30 @@ const anchors = ["Synopsis", "Packages", "Team"] as const;
export const meta = mergeMeta(
(params) => {
const data = params.data as AuthorRes;
const url = BASE_URL + `/author/${encodeURIComponent(data.authorId)}`;

return [
{ title: `${data.authorId} | CRAN/E` },
{
name: "description",
content: `All R packages created by ${data.authorId} for CRAN`,
},
];
},
(params) => {
const data = params.data as AuthorRes;
const url = BASE_URL + `/author/${data.authorId}`;

return [
{
property: "og:image",
content: `${url}/og`,
},
{ property: "og:title", content: `${data.authorId} | CRAN/E` },
{
property: "og:description",
content: `All R packages created by ${data.authorId} for CRAN`,
},
{ property: "og:url", content: url },
];
},
(params) => {
const data = params.data as AuthorRes;

return [
{
"script:ld+json": composeBreadcrumbsJsonLd([
{
Expand Down Expand Up @@ -91,7 +95,7 @@ export const loader: LoaderFunction = async ({ params }) => {
if (!authorId) {
throw new Response(null, {
status: 400,
statusText: "Author ID is required",
statusText: "Valid author ID is required",
});
}

Expand Down
37 changes: 37 additions & 0 deletions web/app/routes/_page.package.$packageId.og.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { LoaderFunctionArgs } from "@remix-run/node";
import { composePackageOGImage } from "../modules/meta-og-image.server";
import { ENV } from "../data/env";
import { addDays, getSeconds } from "date-fns";
import { packageSlugSchema } from "../data/package.shape";

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { origin } = new URL(request.url);
const { packageId } = params;

const parsedId = packageSlugSchema.safeParse(packageId);
if (parsedId.error) {
throw new Response(null, {
status: 400,
statusText: "Valid package ID is required",
});
}

const png = await composePackageOGImage({
name: encodeURIComponent(parsedId.data),
requestUrl: origin,
});

// Respond with the PNG buffer
return new Response(png, {
status: 200,
headers: {
// Tell the browser the response is an image
"Content-Type": "image/png",
// Tip: You might want to heavily cache the response in production
"cache-control":
ENV.NODE_ENV === "production"
? `public, immutable, no-transform, max-age=${getSeconds(addDays(new Date(), 365))}`
: "no-cache",
},
});
};
9 changes: 5 additions & 4 deletions web/app/routes/_page.package.$packageId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,21 @@ const sections = [
export const meta = mergeMeta(
({ data }) => {
const { item } = data as { item: Pkg };
const url = BASE_URL + `/${encodeURIComponent(item.name)}`;

return [
{ title: `${item.name} | CRAN/E` },
{ name: "description", content: item.title },
{ property: "og:title", content: `${item.name} | CRAN/E` },
{ property: "og:description", content: item.title },
{ property: "og:url", content: url },
{ property: "og:image", content: `${url}/og` },
];
},
({ data }) => {
const { item } = data as { item: Pkg };
const url = BASE_URL + `/${item.name}`;

return [
{ property: "og:title", content: `${item.name} | CRAN/E` },
{ property: "og:description", content: item.title },
{ property: "og:url", content: url },
{
"script:ld+json": composeBreadcrumbsJsonLd([
{
Expand Down
2 changes: 1 addition & 1 deletion web/app/routes/_page.press.news._article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Tag } from "../modules/tag";
import { findArticleMatch } from "../modules/article";
import { AnchorLink, Anchors } from "../modules/anchors";

export default function PrivacyPage() {
export default function NewsArticlePage() {
const matches = useMatches();
const article = findArticleMatch(matches);

Expand Down
8 changes: 8 additions & 0 deletions web/app/routes/_page.press.news._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Header } from "../modules/header";
import { mergeMeta } from "../modules/meta";
import { Link } from "@remix-run/react";
import { ArticlePreviewInfoCard } from "../modules/article";
import { BASE_URL } from "../modules/app";

export const handle = {
hasFooter: true,
Expand All @@ -13,6 +14,13 @@ export const meta = mergeMeta(() => {
return [
{ title: "Newsroom | CRAN/E" },
{ name: "description", content: "Latest news and updates of CRAN/E" },
{ property: "og:title", content: "Newsroom | CRAN/E" },
{ property: "og:url", content: `${BASE_URL}/press/news` },
{
property: "og:description",
content: "Latest news and updates of CRAN/E",
},
{ property: "og:image", content: `${BASE_URL}/press/news/og` },
];
});

Expand Down
Loading

0 comments on commit 610b345

Please sign in to comment.