INTEGRITY Dokumentace

Generování OG obrázků pro weby Astro

Obrázky Open Graph (OG) jsou náhledové obrázky, které se zobrazí při sdílení odkazu na sociálních sítích. Místo ručního vytváření těchto obrázků pro každý příspěvek na blogu můžete pomocí Cloudflare Browser Run automaticky generovat značkové náhledové obrázky pro sociální sítě ze šablony Astro.

V tomto tutoriálu:

  1. Vytvořte stránku v Astro, která vykresluje návrh vašeho OG image.
  2. Pomocí Browser Run pořiďte snímek dané stránky ve formátu PNG.
  3. Poskytujte vygenerované obrázky prohledávačům sociálních sítí.

Předpoklady

1. Vytvoření šablony OG obrázku

Vytvořte trasu v Astro, která vykresluje návrh vašeho OG image. Tato stránka je jediným zdrojem pravdy pro rozvržení vašeho obrázku.

Vytvořit src/pages/social-card.astro:

---
export const prerender = false;

const title = Astro.url.searchParams.get("title") || "Untitled";
const image = Astro.url.searchParams.get("image");
const author = Astro.url.searchParams.get("author");
---

<html>
	<head>
		<meta charset="utf-8" />
		<style>
			* {
				margin: 0;
				padding: 0;
				box-sizing: border-box;
			}
			body {
				width: 1200px;
				height: 630px;
				display: flex;
				flex-direction: column;
				justify-content: flex-end;
				padding: 60px;
				font-family: system-ui, sans-serif;
				background: linear-gradient(135deg, #f38020 0%, #f9a825 100%);
				color: white;
			}
			.title {
				font-size: 64px;
				font-weight: bold;
				line-height: 1.1;
				margin-bottom: 24px;
			}
			.author {
				font-size: 24px;
				opacity: 0.9;
			}
			.logo {
				position: absolute;
				top: 60px;
				left: 60px;
				height: 40px;
			}
		</style>
	</head>
	<body>
		<img class="logo" src="/your-logo.png" alt="Your logo" />
		<h1 class="title">{title}</h1>
		{author && <p class="author">By {author}</p>}
	</body>
</html>

Spusťte vývojový server Astro a otestujte šablonu:

npm run dev

Otestujte lokálně tak, že navštívíte http://localhost:4321/social-card?title=My%20Blog%20Post&author=Omar.

Než budete pokračovat, nasaďte svůj web, abyste zajistili /social-card trasa je aktivní:

# For Cloudflare Workers
npx wrangler deploy

Aktualizujte BASE_URL ve skriptu níže tak, aby odpovídal adrese URL vašeho nasazeného webu.

2. Generování OG obrázků při sestavení

Vygenerujte všechny OG obrázky během procesu sestavení Astro pomocí Cloudflare Browser Run Quick Actions.

Vytvořit scripts/generate-social-cards.ts:

import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from "fs";
import { join } from "path";

// Configuration
const BASE_URL = "https://your-site.com"; // Your deployed site URL
const CF_API = "https://api.cloudflare.com/client/v4/accounts";
const OUTPUT_DIR = "public/social-cards"; // Output directory for generated images
const POSTS_DIR = "src/data/posts"; // Directory containing your markdown posts (adjust to match your project)

interface Post {
	slug: string;
	title: string;
	author?: string;
}

/** Extract a frontmatter field value from raw markdown content. */
function getFrontmatterField(content: string, field: string): string | null {
	const match = content.match(new RegExp(`^${field}:\\s*"?([^"\\n]+)"?`, "m"));
	return match ? match[1].trim() : null;
}

/**
 * Read all post files and return { slug, title, author }[].
 * This function scans the POSTS_DIR for markdown files, extracts frontmatter
 * fields (slug, title, author), and returns an array of post objects.
 * Falls back to filename for slug and slug for title if frontmatter is missing.
 */
function readPosts(): Post[] {
	if (!existsSync(POSTS_DIR)) return [];
	const files = readdirSync(POSTS_DIR).filter((f) => f.endsWith(".md"));
	return files.map((file) => {
		const raw = readFileSync(join(POSTS_DIR, file), "utf-8");
		const slug = getFrontmatterField(raw, "slug") ?? file.replace(/\.md$/, "");
		const title = getFrontmatterField(raw, "title") ?? slug;
		const author = getFrontmatterField(raw, "author") ?? undefined;
		return { slug, title, author };
	});
}

/**
 * Capture a screenshot using Cloudflare Browser Run Quick Actions
 */
async function captureScreenshot(
	accountId: string,
	apiToken: string,
	pageUrl: string,
): Promise<ArrayBuffer> {
	const endpoint = `${CF_API}/${accountId}/browser-rendering/screenshot`;

	const res = await fetch(endpoint, {
		method: "POST",
		headers: {
			Authorization: `Bearer ${apiToken}`,
			"Content-Type": "application/json",
		},
		body: JSON.stringify({
			url: pageUrl,
			viewport: { width: 1200, height: 630 }, // Standard OG image size
			gotoOptions: { waitUntil: "networkidle0" }, // Wait for page to fully load
		}),
	});

	if (!res.ok) {
		const text = await res.text();
		throw new Error(`Screenshot API returned ${res.status}: ${text}`);
	}

	return res.arrayBuffer();
}

async function main() {
	// Read credentials from environment variables
	const accountId = process.env.CF_ACCOUNT_ID;
	const apiToken = process.env.CF_API_TOKEN;

	if (!accountId || !apiToken) {
		console.error("Error: CF_ACCOUNT_ID and CF_API_TOKEN required");
		process.exit(1);
	}

	// Check if --force flag is passed to regenerate all images
	const force = process.argv.includes("--force");

	// Read posts from markdown files
	const posts = readPosts();

	if (posts.length === 0) {
		console.log("No posts found. Check your POSTS_DIR path.");
		process.exit(0);
	}

	console.log(`Found ${posts.length} posts to process\n`);

	// Ensure output directory exists
	mkdirSync(OUTPUT_DIR, { recursive: true });

	let generated = 0;
	let skipped = 0;

	// Generate social card for each post
	for (let i = 0; i < posts.length; i++) {
		const post = posts[i];
		const outPath = join(OUTPUT_DIR, `${post.slug}.png`);
		const label = `[${i + 1}/${posts.length}]`;

		// Skip if file exists and --force flag not set
		if (!force && existsSync(outPath)) {
			console.log(`${label} ${post.slug}.png — skipped (exists)`);
			skipped++;
			continue;
		}

		// Build URL with query parameters for the OG template
		const params = new URLSearchParams({
			title: post.title,
			author: post.author || "",
		});
		const url = `${BASE_URL}/social-card?${params}`;

		try {
			// Capture screenshot and save to file
			const png = await captureScreenshot(accountId, apiToken, url);
			writeFileSync(outPath, Buffer.from(png));
			console.log(`${label} ${post.slug}.png — done`);
			generated++;
		} catch (err) {
			console.error(`${label} ${post.slug}.png — failed:`, err);
		}

		// Rate limiting: small delay between requests
		if (i < posts.length - 1) {
			await new Promise((resolve) => setTimeout(resolve, 200));
		}
	}

	console.log(`\nDone. Generated: ${generated}, Skipped: ${skipped}`);
}

main();

Přihlašovací údaje Cloudflare nastavte jako proměnné prostředí:

export CF_ACCOUNT_ID=your_account_id
export CF_API_TOKEN=your_api_token

Spusťte skript pro generování obrázků:

# Generate new images only
bun scripts/generate-social-cards.ts

# Regenerate all images
bun scripts/generate-social-cards.ts --force

Volitelně přidejte do svého build skriptu v package.json:

{
	"scripts": {
		"build": "bun scripts/generate-social-cards.ts && astro build"
	}
}

3. Přidání metaznaček OG na stránky

Upravte šablonu příspěvku na blogu tak, aby odkazovala na vygenerované obrázky:

---
// src/layouts/BlogPost.astro
const { title, slug, author } = Astro.props;
const ogImageUrl = `/social-cards/${slug}.png`;
---

<html>
	<head>
		<meta property="og:title" content={title} />
		<meta property="og:image" content={ogImageUrl} />
		<meta property="og:image:width" content="1200" />
		<meta property="og:image:height" content="630" />
		<meta name="twitter:card" content="summary_large_image" />
		<meta name="twitter:image" content={ogImageUrl} />
	</head>
	<body>
		<slot />
	</body>
</html>

4. Testování OG obrázků

Před testováním nezapomeňte nasadit web s nově vygenerovanými obrázky sociálních karet:

# For Cloudflare Workers
npx wrangler deploy

Pomocí těchto nástrojů ověřte, že se vaše OG obrázky vykreslují správně:

Přizpůsobte šablonu

Přidání obrázku na pozadí

---
const title = Astro.url.searchParams.get("title") || "Untitled";
const image = Astro.url.searchParams.get("image");
---

<body style={image ? `background-image: url(${image})` : undefined}>
	<!-- content -->
</body>

Použití vlastních písem

<head>
	<link
		href="https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap"
		rel="stylesheet"
	/>
	<style>
		body {
			font-family: "Inter", sans-serif;
		}
	</style>
</head>

Přidání Tailwind CSS

Pokud váš web Astro používá Tailwind, můžete ho použít ve své šabloně OG:

---
import "../styles/global.css";
---

<body
	class="flex h-[630px] w-[1200px] flex-col justify-end bg-gradient-to-br from-orange-500 to-amber-500 p-16 text-white"
>
	<h1 class="mb-6 text-6xl leading-tight font-bold">{title}</h1>
</body>

Úvahy o výkonu

Optimalizace obrázků

Zvažte zpracování vygenerovaných obrázků prostřednictvím Cloudflare Images nebo Image Resizing pro další optimalizaci:

const optimizedUrl = `https://your-domain.com/cdn-cgi/image/width=1200,format=auto/social-cards/${slug}.png`;

Další kroky

Váš web Astro nyní automaticky generuje obrázky OG pomocí Browser Run. Když sdílíte odkaz na sociálních sítích, crawlery si vygenerovaný obrázek stáhnou ze statické cesty.

Odtud můžete: