Imagini OG în SvelteKit
Generează imagini OG în SvelteKit cu API-ul FastOG — cu un abonament Dynamic API (creează-ți cheia API în dashboard), bazat pe abonament, securizat prin HMAC. Acest ghid îți arată cum să creezi o utilitate de semnare (signer) pentru URL-uri de imagini OG semnate HMAC și să randezi imagini Open Graph dinamice pe loc. Folosește Free OG Image Tester pentru previzualizare înainte de integrare.
Pornire rapidă
1. Creează utilitarul de semnare
// src/lib/og-image-signer.js
import crypto from "node:crypto";
export function generateSignedOgUrl(params, options = {}) {
const {
baseUrl = "https://fastog.com/api/v1/og",
apiKey = process.env.FASTOG_API_KEY,
hmacSecret = process.env.FASTOG_HMAC_SECRET,
} = options;
if (!apiKey || !hmacSecret) {
throw new Error("Missing FASTOG_API_KEY or FASTOG_HMAC_SECRET");
}
// RFC-3986 percent-encoding: keep A-Za-z0-9-._~
const enc = (s) =>
encodeURIComponent(s).replace(
/[!'()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
);
// Every query param except the signature, INCLUDING key
const all = { ...params, key: apiKey };
// Canonical query: sorted enc(key)=enc(value) pairs joined with &
const pairs = [];
for (const [k, v] of Object.entries(all)) {
for (const vv of Array.isArray(v) ? v : [v]) {
pairs.push(`${enc(k)}=${enc(vv)}`);
}
}
pairs.sort();
const canonical = pairs.join("&");
// Sign the exact bytes the server will verify
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(`GET/api/v1/og\n${canonical}`)
.digest("hex");
// Build the URL from the same pairs so bytes match the canonical form
const url = new URL(baseUrl);
for (const [k, v] of Object.entries(all)) {
for (const vv of Array.isArray(v) ? v : [v]) {
url.searchParams.append(Array.isArray(v) ? `${k}[]` : k, vv);
}
}
url.searchParams.set("s", signature);
return url.toString();
}
2. Variabile de mediu
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
Pagină cu metadate
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
export let data;
$: ({ post, ogImageUrl } = data);
</script>
<svelte:head>
<title>{post.title}</title>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
</svelte:head>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
</article>
// src/routes/blog/[slug]/+page.server.js
import { generateSignedOgUrl } from "$lib/og-image-signer";
export async function load({ params }) {
const post = await getPost(params.slug);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: post.title,
subtitle: post.excerpt,
author: post.author.name,
});
return {
post,
ogImageUrl,
};
}
Rută API
// src/routes/api/og/+server.js
import { json } from "@sveltejs/kit";
import { generateSignedOgUrl } from "$lib/og-image-signer";
export async function GET({ url }) {
const title = url.searchParams.get("title");
const template = url.searchParams.get("template") || "default";
if (!title) {
return json({ error: "Missing title" }, { status: 400 });
}
const ogImageUrl = generateSignedOgUrl({
template,
title,
});
return json({ ogImageUrl });
}
Exemplu de comerț electronic
// src/routes/products/[id]/+page.server.js
import { generateSignedOgUrl } from "$lib/og-image-signer";
export async function load({ params }) {
const product = await getProduct(params.id);
const ogImageUrl = generateSignedOgUrl({
template: "ecommerce",
title: product.name,
price: product.price,
image: product.image,
});
return { product, ogImageUrl };
}
<!-- src/routes/products/[id]/+page.svelte -->
<script>
export let data;
$: ({ product, ogImageUrl } = data);
</script>
<svelte:head>
<meta property="og:image" content={ogImageUrl} />
<meta property="og:title" content={product.name} />
</svelte:head>
<main>
<h1>{product.name}</h1>
<p>{product.price}</p>
</main>
Profil utilizator (Șablon Blog)
FastOG nu are un șablon profile — folosește blog pentru un card de tip profil, transmițând numele utilizatorului ca title, biografia ca subtitle și numele ca author.
// src/routes/users/[username]/+page.server.js
import { generateSignedOgUrl } from "$lib/og-image-signer";
export async function load({ params }) {
const user = await getUser(params.username);
const ogImageUrl = generateSignedOgUrl({
template: "blog",
title: user.name,
subtitle: user.bio,
author: user.name,
});
return { user, ogImageUrl };
}
Componentă cu Imagine OG Dinamică
<!-- src/lib/ShareButtons.svelte -->
<script>
import { onMount } from "svelte";
export let post;
let ogImageUrl = null;
onMount(async () => {
const res = await fetch(`/api/og?title=${encodeURIComponent(post.title)}`);
const data = await res.json();
ogImageUrl = data.ogImageUrl;
});
</script>
<div class="share-buttons">
<button on:click={() => shareToTwitter(ogImageUrl)}>
Share on Twitter
</button>
</div>
Capcane Frecvente
Semnarea pe Partea Clientului
Nu semna niciodată URL-urile OG în codul din browser — secretul HMAC ar fi expus.
❌ Greșit:
<script>
import { generateSignedOgUrl } from "$lib/og-image-signer";
// This runs in the browser!
const ogUrl = generateSignedOgUrl({ title: post.title });
</script>
✅ Corect:
// +page.server.js (runs on server only)
import { generateSignedOgUrl } from "$lib/og-image-signer";
export async function load({ params }) {
const ogImageUrl = generateSignedOgUrl({ title: post.title });
return { ogImageUrl };
}
Semnarea unor Octeți Diferiți de Cei pe Care Îi Trimiți
Semnătura acoperă GET/api/v1/og plus un caracter de linie nouă plus interogarea canonică — perechile enc(key)=enc(value) sortate ale fiecărui parametru, cu excepția s, inclusiv key. Construiește URL-ul final din aceleași perechi codificate, altfel serverul returnează 401 invalid_signature.