Opengraph images

This commit is contained in:
taskylizard
2023-11-06 21:22:33 +05:30
parent 7213a1f4af
commit 4bc7f038e1
12 changed files with 706 additions and 19 deletions

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string; dir?: string }>();
</script>
<template>
<div
tw="w-full h-full bg-black flex flex-col"
style="background-image: url(https://files.catbox.moe/1f84dy.png)">
<div tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between">
<div tw="w-full flex justify-between items-center text-5xl font-medium">
<div tw="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 256 256">
<path
fill="#f3f4f6"
d="M240 128a15.74 15.74 0 0 1-7.6 13.51L88.32 229.65a16 16 0 0 1-16.2.3A15.86 15.86 0 0 1 64 216.13V39.87a15.86 15.86 0 0 1 8.12-13.82a16 16 0 0 1 16.2.3l144.08 88.14A15.74 15.74 0 0 1 240 128Z" />
</svg>
<div tw="text-zinc-100 ml-2 mt-1 font-semibold">freemediaheckyeah</div>
</div>
<div tw="flex items-center text-zinc-300">
<div tw="text-4xl font-semibold mr-2" v-html="dir" />
</div>
</div>
<div tw="w-full pr-56 flex flex-col items-start justify-end">
<div tw="text-6xl font-bold text-stone-200" v-html="title" />
<div v-if="description" tw="mt-2 text-4xl text-neutral-400" v-html="description" />
</div>
</div>
<div tw="shrink-0 h-4 w-full flex" style="background-color: #7bc5e4" />
</div>
</template>

View File

@@ -0,0 +1,6 @@
/**
* Barrel generated using @taskylizard/tasker.
*/
export * from "./meta";
export * from "./opengraph";

View File

@@ -10,10 +10,10 @@ export function generateMeta(context: TransformContext, hostname: string) {
head.push(["meta", { property: "og:url", content: url }]);
head.push(["meta", { name: "twitter:url", content: url }]);
head.push(["meta", { name: "twitter:card", content: "summary_large_image" }]);
head.push(["meta", { name: "theme-color", content: "#7bc5e4" }]);
head.push(["meta", { property: "og:type", content: "website" }]);
if (pageData.frontmatter.description) {
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }]);
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }]);
head.push([
"meta",
{
@@ -28,10 +28,7 @@ export function generateMeta(context: TransformContext, hostname: string) {
content: pageData.frontmatter.description,
},
]);
}
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }]);
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }]);
if (pageData.frontmatter.image) {
head.push([
"meta",
@@ -47,8 +44,32 @@ export function generateMeta(context: TransformContext, hostname: string) {
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
]);
}
} else {
const url = pageData.filePath.replace("index.md", "").replace(".md", "");
const imageUrl = `${url}/__og_image__/og.png`.replace(/\/\//g, "/").replace(/^\//, "");
head.push(["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }]);
head.push(["meta", { property: "og:image:width", content: "1200" }]);
head.push(["meta", { property: "og:image:height", content: "628" }]);
head.push(["meta", { property: "og:image:type", content: "image/png" }]);
head.push(["meta", { property: "og:image:alt", content: pageData.frontmatter.title }]);
head.push(["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }]);
head.push(["meta", { name: "twitter:image:width", content: "1200" }]);
head.push(["meta", { name: "twitter:image:height", content: "628" }]);
head.push(["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }]);
}
if (pageData.frontmatter.tag) {
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }]);
}
if (pageData.frontmatter.date) {
head.push([
"meta",
{
property: "article:published_time",
content: pageData.frontmatter.date,
},
]);
}
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
head.push([
"meta",

View File

@@ -0,0 +1,103 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createContentLoader } from "vitepress";
import type { ContentData, SiteConfig } from "vitepress";
import { type SatoriOptions, satoriVue } from "x-satori/vue";
import { renderAsync } from "@resvg/resvg-js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const __fonts = resolve(__dirname, "../fonts");
export async function generateImages(config: SiteConfig) {
const pages = await createContentLoader("**/*.md", { excerpt: true }).load();
const template = await readFile(resolve(__dirname, "./Template.vue"), "utf-8");
const fonts: SatoriOptions["fonts"] = [
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
weight: 400,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
weight: 500,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
weight: 600,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
weight: 700,
style: "normal",
},
];
const filteredPages = pages.filter((p) => p.frontmatter.image === undefined);
for (const page of filteredPages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts,
});
}
}
interface GenerateImagesOptions {
page: ContentData;
template: string;
outDir: string;
fonts: SatoriOptions["fonts"];
}
function getDir(url: string) {
if (url.startsWith("/glossary/")) {
return "Glossary";
} else if (url.startsWith("/guides/")) {
return "Guide";
}
// Means we are at root.
return undefined;
}
async function generateImage({ page, template, outDir, fonts }: GenerateImagesOptions) {
const { frontmatter, url } = page;
const options: SatoriOptions = {
width: 1200,
height: 628,
fonts,
props: {
title:
frontmatter.layout === "home"
? frontmatter.hero.name ?? frontmatter.title
: frontmatter.title,
description:
frontmatter.layout === "home"
? frontmatter.hero.tagline ?? frontmatter.description
: frontmatter.description,
dir: getDir(url),
},
};
const svg = await satoriVue(options, template);
const render = await renderAsync(svg);
const outputFolder = resolve(outDir, url.substring(1), "__og_image__");
const outputFile = resolve(outputFolder, "og.png");
await mkdir(outputFolder, { recursive: true });
return await writeFile(outputFile, render.asPng());
}

View File

@@ -0,0 +1,45 @@
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { SatoriOptions, defineSatoriConfig } from "x-satori/vue";
const __dirname = dirname(fileURLToPath(import.meta.url));
const __fonts = resolve(__dirname, "../fonts");
const fonts: SatoriOptions["fonts"] = [
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
weight: 400,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
weight: 500,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
weight: 600,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
weight: 700,
style: "normal",
},
];
export default defineSatoriConfig({
width: 1200,
height: 628,
fonts,
props: {
title: "Title",
description:
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.",
dir: "/j",
},
});