diff --git a/app/cases/[slug]/opengraph-image.tsx b/app/cases/[slug]/opengraph-image.tsx index 64e1165..6a24f83 100644 --- a/app/cases/[slug]/opengraph-image.tsx +++ b/app/cases/[slug]/opengraph-image.tsx @@ -1,6 +1,11 @@ import { notFound } from "next/navigation"; import { ImageResponse } from "next/og"; -import { OG_CONTENT_TYPE, OG_SIZE, OgCard } from "@/lib/og-card"; +import { + OG_CONTENT_TYPE, + OG_SIZE, + OgCard, + readProjectOgBackground, +} from "@/lib/og-card"; import { caseStudies } from "@/lib/site-content"; function getCaseStudy(slug: string) { @@ -46,6 +51,7 @@ export default async function Image({ eyebrow="Case de engenharia de software" title={caseStudy.title} description={caseStudy.summary} + backgroundImage={readProjectOgBackground(caseStudy.id)} />, OG_SIZE, ); diff --git a/lib/og-card.test.tsx b/lib/og-card.test.tsx new file mode 100644 index 0000000..8874c33 --- /dev/null +++ b/lib/og-card.test.tsx @@ -0,0 +1,36 @@ +import { render } from "@testing-library/react"; +import { OgCard, readProjectOgBackground } from "./og-card"; + +describe("readProjectOgBackground", () => { + it("returns the recorded cover of a showcase project as a JPEG data URI", () => { + const uri = readProjectOgBackground("nutriscan"); + expect(uri).toMatch(/^data:image\/jpeg;base64,/); + expect((uri ?? "").length).toBeGreaterThan(5_000); + }); + + it("returns undefined for projects without a recorded cover", () => { + expect(readProjectOgBackground("twins-lab")).toBeUndefined(); + expect(readProjectOgBackground("../../etc/passwd")).toBeUndefined(); + }); +}); + +describe("OgCard", () => { + const props = { eyebrow: "Case", title: "Título", description: "Resumo." }; + + it("keeps the plain dark card when there is no background", () => { + const { container } = render(); + // só a marca + expect(container.querySelectorAll("img")).toHaveLength(1); + }); + + it("puts the project cover behind the text with a darkening gradient", () => { + const { container } = render( + , + ); + const images = [...container.querySelectorAll("img")]; + expect(images).toHaveLength(2); + expect(images[0]).toHaveAttribute("src", "data:image/jpeg;base64,AAAA"); + expect(images[0]).toHaveAttribute("alt", ""); + expect(container.innerHTML).toContain("linear-gradient"); + }); +}); diff --git a/lib/og-card.tsx b/lib/og-card.tsx index 36f0e98..7b1821d 100644 --- a/lib/og-card.tsx +++ b/lib/og-card.tsx @@ -1,5 +1,6 @@ -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { getOgImagePath, PROJECT_VIDEO_IDS } from "./project-showcase"; const brandMark = `data:image/png;base64,${readFileSync( join(process.cwd(), "public/brand/mark-dark.png"), @@ -8,14 +9,25 @@ const brandMark = `data:image/png;base64,${readFileSync( export const OG_SIZE = { width: 1200, height: 630 } as const; export const OG_CONTENT_TYPE = "image/png"; +/** Capa 1200x630 gravada da página do projeto (public/videos//og.jpg), como data URI; `undefined` se não existir. */ +export function readProjectOgBackground(id: string): string | undefined { + if (!PROJECT_VIDEO_IDS.includes(id)) return undefined; + const path = join(process.cwd(), "public", getOgImagePath(id)); + if (!existsSync(path)) return undefined; + return `data:image/jpeg;base64,${readFileSync(path).toString("base64")}`; +} + export function OgCard({ eyebrow, title, description, + backgroundImage, }: { eyebrow: string; title: string; description: string; + /** Imagem de fundo (data URI), escurecida da esquerda para a direita para o texto continuar legível. */ + backgroundImage?: string; }) { return (
+ {backgroundImage && ( + <> + {/* biome-ignore lint/performance/noImgElement: o ImageResponse (satori) só desenha */} + +
+ + )}