feat(social): add post image thumbnail to OG social card
Ports the same loadPostImage + thumbBlock pattern from the calendar social route. Reads postImage.src (fallback: image.src), loads via CMS transform API as base64 JPEG, renders next to QR code. Posts without an image are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,7 +98,7 @@ function formatDateLong(iso: string | undefined | null): string {
|
||||
|
||||
const cache = new Map<string, { png: Buffer; ts: number }>();
|
||||
const CACHE_TTL = 10 * 60_000;
|
||||
const CACHE_VERSION = "v1";
|
||||
const CACHE_VERSION = "v2";
|
||||
|
||||
function parseFormat(v: string | null): Format {
|
||||
if (v === "square") return "square";
|
||||
@@ -111,6 +111,24 @@ function parseTheme(v: string | null): Theme {
|
||||
return "wald";
|
||||
}
|
||||
|
||||
async function loadPostImage(
|
||||
imageSrc: string | undefined,
|
||||
w: number,
|
||||
h: number,
|
||||
): Promise<string | null> {
|
||||
if (!imageSrc) return null;
|
||||
try {
|
||||
const cmsBase = (env.PUBLIC_CMS_URL || "http://localhost:3000").replace(/\/$/, "");
|
||||
const transformUrl = `${cmsBase}/api/transform?url=${encodeURIComponent(imageSrc)}&w=${w}&h=${h}&fit=inside&format=jpeg&quality=80`;
|
||||
const res = await fetch(transformUrl);
|
||||
if (!res.ok) return null;
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
return `data:image/jpeg;base64,${buf.toString("base64")}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/</g, "‹").replace(/>/g, "›");
|
||||
}
|
||||
@@ -150,12 +168,24 @@ export const GET: RequestHandler = async ({ params, url, setHeaders }) => {
|
||||
tags?: unknown;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
postImage?: { src?: string } | string;
|
||||
image?: { src?: string } | string;
|
||||
};
|
||||
|
||||
const { w, h } = SIZES[formatParam];
|
||||
const fonts = await loadFonts();
|
||||
const logoSvg = await loadLogo();
|
||||
|
||||
const rawImageSrc = post.postImage ?? post.image;
|
||||
const imageSrc =
|
||||
typeof rawImageSrc === "string"
|
||||
? rawImageSrc
|
||||
: rawImageSrc && typeof rawImageSrc === "object"
|
||||
? (rawImageSrc as { src?: string }).src
|
||||
: undefined;
|
||||
const thumbSize = formatParam === "story" ? 280 : 180;
|
||||
const postThumb = imageSrc ? await loadPostImage(imageSrc, thumbSize * 2, thumbSize * 2) : null;
|
||||
|
||||
const headline = (post.headline ?? post.linkName ?? "").trim();
|
||||
const subheadline = (post.subheadline ?? "").trim();
|
||||
const excerpt = (post.excerpt ?? "").trim();
|
||||
@@ -302,12 +332,19 @@ export const GET: RequestHandler = async ({ params, url, setHeaders }) => {
|
||||
`;
|
||||
|
||||
const qrAlign = verticalStack ? "flex-start" : "center";
|
||||
const thumbMax = qrSize + 28;
|
||||
const thumbBlock = postThumb
|
||||
? `<div style="display:flex;align-items:center;justify-content:center;max-width:${thumbMax}px;max-height:${thumbMax}px;"><img src="${postThumb}" style="max-width:${thumbMax}px;max-height:${thumbMax}px;object-fit:contain;" /></div>`
|
||||
: "";
|
||||
const qrBlock = `
|
||||
<div style="display:flex;flex-direction:column;align-items:${qrAlign};gap:8px;">
|
||||
<div style="display:flex;background:${palette.qrBoxBg};padding:14px;border-radius:12px;">
|
||||
<img src="${qrDataUrl}" style="width:${qrSize}px;height:${qrSize}px;" />
|
||||
<div style="display:flex;align-items:flex-start;gap:${verticalStack ? 16 : 20}px;">
|
||||
<div style="display:flex;flex-direction:column;align-items:${qrAlign};gap:8px;">
|
||||
<div style="display:flex;background:${palette.qrBoxBg};padding:14px;border-radius:12px;">
|
||||
<img src="${qrDataUrl}" style="width:${qrSize}px;height:${qrSize}px;" />
|
||||
</div>
|
||||
<div style="display:flex;font-size:${isStory ? 22 : 16}px;color:${palette.textDim};font-weight:500;letter-spacing:0.05em;">Beitrag lesen</div>
|
||||
</div>
|
||||
<div style="display:flex;font-size:${isStory ? 22 : 16}px;color:${palette.textDim};font-weight:500;letter-spacing:0.05em;">Beitrag lesen</div>
|
||||
${thumbBlock}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user