feat(og): capa do projeto como fundo da imagem de compartilhamento dos cases
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
540fe9f87a
commit
ec760330d4
3 files changed
+80
-2
No files matched your search
@@ -1,6 +1,11 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ImageResponse } from "next/og";
|
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";
|
import { caseStudies } from "@/lib/site-content";
|
||||||
|
|
||||||
function getCaseStudy(slug: string) {
|
function getCaseStudy(slug: string) {
|
||||||
@@ -46,6 +51,7 @@ export default async function Image({
|
|||||||
eyebrow="Case de engenharia de software"
|
eyebrow="Case de engenharia de software"
|
||||||
title={caseStudy.title}
|
title={caseStudy.title}
|
||||||
description={caseStudy.summary}
|
description={caseStudy.summary}
|
||||||
|
backgroundImage={readProjectOgBackground(caseStudy.id)}
|
||||||
/>,
|
/>,
|
||||||
OG_SIZE,
|
OG_SIZE,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(<OgCard {...props} />);
|
||||||
|
// só a marca
|
||||||
|
expect(container.querySelectorAll("img")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puts the project cover behind the text with a darkening gradient", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<OgCard {...props} backgroundImage="data:image/jpeg;base64,AAAA" />,
|
||||||
|
);
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
+37
-1
@@ -1,5 +1,6 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { getOgImagePath, PROJECT_VIDEO_IDS } from "./project-showcase";
|
||||||
|
|
||||||
const brandMark = `data:image/png;base64,${readFileSync(
|
const brandMark = `data:image/png;base64,${readFileSync(
|
||||||
join(process.cwd(), "public/brand/mark-dark.png"),
|
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_SIZE = { width: 1200, height: 630 } as const;
|
||||||
export const OG_CONTENT_TYPE = "image/png";
|
export const OG_CONTENT_TYPE = "image/png";
|
||||||
|
|
||||||
|
/** Capa 1200x630 gravada da página do projeto (public/videos/<id>/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({
|
export function OgCard({
|
||||||
eyebrow,
|
eyebrow,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
backgroundImage,
|
||||||
}: {
|
}: {
|
||||||
eyebrow: string;
|
eyebrow: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
/** Imagem de fundo (data URI), escurecida da esquerda para a direita para o texto continuar legível. */
|
||||||
|
backgroundImage?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -32,6 +44,30 @@ export function OgCard({
|
|||||||
fontFamily: "Arial, sans-serif",
|
fontFamily: "Arial, sans-serif",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{backgroundImage && (
|
||||||
|
<>
|
||||||
|
{/* biome-ignore lint/performance/noImgElement: o ImageResponse (satori) só desenha <img> */}
|
||||||
|
<img
|
||||||
|
src={backgroundImage}
|
||||||
|
alt=""
|
||||||
|
width={OG_SIZE.width}
|
||||||
|
height={OG_SIZE.height}
|
||||||
|
style={{ position: "absolute", top: 0, left: 0 }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: OG_SIZE.width,
|
||||||
|
height: OG_SIZE.height,
|
||||||
|
display: "flex",
|
||||||
|
background:
|
||||||
|
"linear-gradient(90deg, rgba(5,5,5,0.97) 0%, rgba(5,5,5,0.9) 48%, rgba(5,5,5,0.58) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<img
|
<img
|
||||||
src={brandMark}
|
src={brandMark}
|
||||||
alt="Evandro.dev.br"
|
alt="Evandro.dev.br"
|
||||||
|
|||||||
Reference in new issue
Block a user