-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds support for Open Graph images
- Loading branch information
Showing
9 changed files
with
291 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { z } from "zod"; | ||
|
||
export const Model = z.object({ | ||
id: z.string(), | ||
category: z.object({ id: z.string(), name: z.string() }), | ||
title: z.string(), | ||
installCommand: z.string(), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { findById, getCategories } from "#workflows"; | ||
import { err, ok } from "neverthrow"; | ||
import { getBaseUrl } from "~/utils/get-base-url.server"; | ||
import { Model } from "./model"; | ||
|
||
export async function getModel( | ||
request: Request, | ||
category: string, | ||
slug: string, | ||
) { | ||
const baseUrl = getBaseUrl(request); | ||
const id = `${category}/${slug}`; | ||
|
||
const workflow = findById(id); | ||
|
||
const installCommand = `curl -s ${baseUrl}/${id} | sh`; | ||
|
||
const currentCatgory = getCategories().find((c) => c.id === category); | ||
|
||
const model = Model.safeParse({ | ||
id, | ||
category: currentCatgory, | ||
title: workflow.title, | ||
installCommand, | ||
}); | ||
|
||
if (!model.success) { | ||
return err(new Error(`failed to prepare view model: ${model.error}`)); | ||
} | ||
|
||
return ok(model.data); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import type { LoaderFunctionArgs } from "@remix-run/node"; | ||
import { z } from "zod"; | ||
import { makeGenerateOgImage } from "~/utils/create-og-image.server"; | ||
import { getModel } from "./query"; | ||
|
||
const ParamsSchema = z.object({ | ||
category: z.string(), | ||
slug: z.string(), | ||
}); | ||
|
||
export async function loader(args: LoaderFunctionArgs) { | ||
const params = ParamsSchema.safeParse(args.params); | ||
|
||
if (!params.success) { | ||
throw new Response("Bad Request", { status: 400 }); | ||
} | ||
|
||
const fetchModelResult = await getModel( | ||
args.request, | ||
params.data.category, | ||
params.data.slug, | ||
); | ||
|
||
if (fetchModelResult.isErr()) { | ||
console.error(fetchModelResult.error); | ||
|
||
throw new Response("Internal Server Error", { status: 500 }); | ||
} | ||
|
||
const model = fetchModelResult.value; | ||
|
||
const createOgImage = makeGenerateOgImage(); | ||
|
||
const resultOfGeneratingImage = await createOgImage({ | ||
title: model.title, | ||
subtitle: `GitHub Actions ${model.category.name} Starter Workflow`, | ||
shell: model.installCommand, | ||
}); | ||
|
||
if (resultOfGeneratingImage.isErr()) { | ||
throw new Response("Internal Server Error", { status: 500 }); | ||
} | ||
|
||
const { image } = resultOfGeneratingImage.value; | ||
|
||
return new Response(image, { | ||
headers: { | ||
"Content-Type": "image/png", | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import type { SatoriOptions } from "satori"; | ||
|
||
import satori from "satori"; | ||
|
||
import { Resvg } from "@resvg/resvg-js"; | ||
import { err, ok } from "neverthrow"; | ||
|
||
type GenerateOgImageRequest = Readonly<{ | ||
title: string; | ||
subtitle: string; | ||
shell?: string; | ||
}>; | ||
|
||
// hypercolor gradients (https://hypercolor.dev) | ||
const backgrounds = [ | ||
{ | ||
name: "Hypher", | ||
css: "linear-gradient(to right top, rgb(236, 72, 153), rgb(239, 68, 68), rgb(234, 179, 8))", | ||
}, | ||
{ | ||
name: "Midnight", | ||
css: "linear-gradient(to right top, rgb(29, 78, 216), rgb(30, 64, 175), rgb(17, 24, 39))", | ||
}, | ||
{ | ||
name: "Flamingo", | ||
css: "linear-gradient(to right top, rgb(244, 114, 182), rgb(219, 39, 119))", | ||
}, | ||
{ | ||
name: "Solid Blue", | ||
css: "linear-gradient(to left, rgb(59, 130, 246), rgb(37, 99, 235))", | ||
}, | ||
{ | ||
name: "Picture", | ||
css: "linear-gradient(to left bottom, rgb(217, 70, 239), rgb(220, 38, 38), rgb(251, 146, 60))", | ||
}, | ||
{ | ||
name: "Video", | ||
css: "linear-gradient(to left top, rgb(239, 68, 68), rgb(153, 27, 27))", | ||
}, | ||
{ | ||
name: "Pink Neon", | ||
css: "linear-gradient(to right, rgb(192, 38, 211), rgb(219, 39, 119))", | ||
}, | ||
{ | ||
name: "Purple Haze", | ||
css: "linear-gradient(to left, rgb(107, 33, 168), rgb(76, 29, 149), rgb(107, 33, 168))", | ||
}, | ||
{ | ||
name: "Emerald", | ||
css: "linear-gradient(to right, rgb(16, 185, 129), rgb(101, 163, 13))", | ||
}, | ||
{ | ||
name: "Salem", | ||
css: "linear-gradient(to top, rgb(17, 24, 39), rgb(88, 28, 135), rgb(124, 58, 237))", | ||
}, | ||
]; | ||
|
||
async function getFont(font: string, weights = [400, 500, 600, 700]) { | ||
const css = await fetch( | ||
`https://fonts.googleapis.com/css2?family=${font}:wght@${weights.join( | ||
";", | ||
)}`, | ||
{ | ||
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", | ||
}, | ||
}, | ||
).then((response) => response.text()); | ||
const resource = css.matchAll( | ||
/src: url\((.+)\) format\('(opentype|truetype)'\)/g, | ||
); | ||
return Promise.all( | ||
[...resource] | ||
.map((match) => match[1]) | ||
.map((url) => fetch(url).then((response) => response.arrayBuffer())) | ||
.map(async (buffer, i) => ({ | ||
name: font, | ||
style: "normal", | ||
weight: weights[i], | ||
data: await buffer, | ||
})), | ||
) as Promise<SatoriOptions["fonts"]>; | ||
} | ||
|
||
declare module "react" { | ||
interface HTMLAttributes<T> { | ||
tw?: string; | ||
} | ||
} | ||
|
||
export function makeGenerateOgImage() { | ||
// Random background gradient | ||
const background = | ||
backgrounds[Math.floor(Math.random() * backgrounds.length)]; | ||
|
||
return async (request: GenerateOgImageRequest) => { | ||
try { | ||
const template = ( | ||
<div | ||
tw="h-full w-full flex items-start justify-start" | ||
style={{ | ||
background: background.css, | ||
}} | ||
> | ||
<div tw="flex items-start justify-start h-full"> | ||
<div tw="flex flex-col justify-between w-full h-full p-4"> | ||
<div tw="flex flex-col bg-white/70 h-full rounded-3xl justify-center"> | ||
<h1 tw="flex justify-center text-3xl font-extrabold text-center tracking-[-2px] leading-none px-8"> | ||
get | ||
<span tw="text-[#e11d48]">actions</span>.dev | ||
</h1> | ||
|
||
<h2 tw="flex justify-center text-7xl font-extrabold opacity-90 tracking-tight leading-10"> | ||
{request.title} | ||
</h2> | ||
<p tw="flex justify-center text-3xl font-medium"> | ||
{request.subtitle} | ||
</p> | ||
|
||
{request.shell ? ( | ||
<div tw="flex bg-white justify-center mt-8 mx-38 rounded-2xl border-[#e11d48]"> | ||
<pre tw="text-xl font-mono">{request.shell}</pre> | ||
</div> | ||
) : null} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
|
||
const svg = await satori(template, { | ||
width: 1200, | ||
height: 630, | ||
fonts: await getFont("Inter"), | ||
}); | ||
|
||
const resvg = new Resvg(svg); | ||
const pngData = resvg.render(); | ||
const image = pngData.asPng(); | ||
|
||
return ok({ image }); | ||
} catch (cause) { | ||
return err(cause instanceof Error ? cause : new Error(cause as string)); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters