Skip to content

Commit

Permalink
feat(social): Add generic OpenGraph image
Browse files Browse the repository at this point in the history
  • Loading branch information
heyhippari committed Nov 13, 2024
1 parent 8c18c45 commit d533d62
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { loadGoogleFont } from '@/core/utils/social';
import { viewsService } from '@/services/views.service';
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export const alt = 'Kanojo';

export const contentType = 'image/png';

/**
* Generates an Open Graph image response for the Kanojo application.
*
* This function creates an image response with a styled div containing
* a title and a description. The image is intended to be used as an
* Open Graph image for social media sharing.
* @returns An ImageResponse object.
*/
export default async function Image() {
const counts = await viewsService.getCurrentCounts();

return new ImageResponse(
(
<div tw="flex flex-row w-full h-full items-center justify-center bg-pink-200">
<div tw="flex flex-col items-center justify-center mr-30">
<h1 tw="self-center whitespace-nowrap border-l-8 border-l-pink-500 pl-3 text-9xl font-extrabold">
Kanojo
</h1>
<p tw="text-4xl font-bold text-center mb-0">
A community-run database
</p>
<p tw="text-4xl font-bold text-center mt-0">for gravure idols.</p>
</div>
<div tw="flex flex-col border-l-4 border-pink-500 pl-24 mr-8">
<div tw="flex flex-col">
<h2 tw="text-2xl font-bold mb-0">Movies</h2>
<p tw="text-6xl font-black text-pink-600 mt-2">
{counts?.movie_count}
</p>
</div>
<div tw="flex flex-col">
<h2 tw="text-2xl font-bold mb-0">People</h2>
<p tw="text-6xl font-black text-pink-600 mt-2">
{counts?.person_count}
</p>
</div>
<div tw="flex flex-col">
<h2 tw="text-2xl font-bold mb-0">Studios</h2>
<p tw="text-6xl font-black text-pink-600 mt-2">
{counts?.studio_count}
</p>
</div>
</div>
</div>
),
{
fonts: [
{
data: await loadGoogleFont('Noto+Sans+JP', '700'),
name: 'Noto Sans JP',
style: 'normal',
},
{
data: await loadGoogleFont('Noto+Sans+JP', '800'),
name: 'Noto Sans JP',
style: 'normal',
},
{
data: await loadGoogleFont('Noto+Sans+JP', '900'),
name: 'Noto Sans JP',
style: 'normal',
},
],
height: 630,
width: 1200,
},
);
}
34 changes: 34 additions & 0 deletions src/core/utils/social.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Asynchronously loads a Google Font and returns its data as an ArrayBuffer.
* @param font - The name of the Google Font to load.
* @param weight - The weight of the font to load.
* @returns A promise that resolves to an ArrayBuffer containing the font data.
* @throws Will throw an error if the font data could not be loaded.
*/
export async function loadGoogleFont(
font: string,
weight:
| "100"
| "200"
| "300"
| "400"
| "500"
| "600"
| "700"
| "800"
| "900" = "400",
): Promise<ArrayBuffer> {
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weight}`;
const cssFile = await fetch(url);
const css = await cssFile.text();
const resource = /src: url\((.+)\) format\('(opentype|truetype)'\)/.exec(css);

if (resource) {
const response = await fetch(resource[1]);
if (response.status == 200) {
return await response.arrayBuffer();
}
}

throw new Error("failed to load font data");
}
77 changes: 77 additions & 0 deletions src/core/utils/validation/api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";

import {
apiMovieSearchResultSchema,
apiMovieSearchResultSchemaArray,
} from "./api";

describe("API schema - movie search result", () => {
it("should validate a correct movie search result", () => {
const validData = {
id: 1,
original_title: "Inception",
release_date: "2010-07-16",
title: "Inception",
};

expect(() => apiMovieSearchResultSchema.parse(validData)).not.toThrow();
});

it("should invalidate an incorrect movie search result", () => {
const invalidData = {
id: "1", // id should be a number
original_title: "Inception",
};

expect(() => apiMovieSearchResultSchema.parse(invalidData)).toThrow();
});

it("should validate a movie search result with optional fields", () => {
const validData = {
id: 1,
original_title: "Inception",
};

expect(() => apiMovieSearchResultSchema.parse(validData)).not.toThrow();
});
});

describe("API schema - movie search result array", () => {
it("should validate an array of correct movie search results", () => {
const validDataArray = [
{
id: 1,
original_title: "Inception",
release_date: "2010-07-16",
title: "Inception",
},
{
id: 2,
original_title: "The Matrix",
release_date: "1999-03-31",
title: "The Matrix",
},
];

expect(() => apiMovieSearchResultSchemaArray.parse(validDataArray)).not
.toThrow();
});

it("should invalidate an array with an incorrect movie search result", () => {
const invalidDataArray = [
{
id: 1,
original_title: "Inception",
release_date: "2010-07-16",
title: "Inception",
},
{
id: "2", // id should be a number
original_title: "The Matrix",
},
];

expect(() => apiMovieSearchResultSchemaArray.parse(invalidDataArray))
.toThrow();
});
});

0 comments on commit d533d62

Please sign in to comment.