OG-kuvat SvelteKitissä
Luo OG-kuvia SvelteKitissä FastOG API:n avulla — Dynamic API -tilauksella (luo API-avaimesi hallintapaneelissa), tilauspohjainen, HMAC-suojattu. Tämä opas näyttää, miten luot allekirjoittaja-apuohjelman (signer utility) HMAC-allekirjoitettuja OG-kuva-URL:ita varten ja renderöit dynaamisia Open Graph -kuvia lennossa. Käytä ilmaista OG-kuvien testaajaa (Free OG Image Tester) esikatseluun ennen integrointia.
Pika-aloitus
1. Luo allekirjoittajan apuohjelma
// 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. Ympäristömuuttujat
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
Sivu, jossa on metatiedot
<!-- 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,
};
}
API-reitti
// 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 });
}
Verkkokauppa-esimerkki
// 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>
Käyttäjäprofiili (blogipohja)
FastOG:ssa ei ole profile-pohjaa — käytä blog-pohjaa profiilityyliseen korttiin, välittämällä käyttäjän nimi title-parametrina, elämäkerta subtitle-parametrina ja nimi author-parametrina.
// 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 };
}
Komponentti dynaamisella OG-kuvalla
<!-- 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>
Yleiset sudenkuopat
Allekirjoitus selaimen puolella
Älä koskaan allekirjoita OG-URL:itä selaimen koodissa — HMAC-salaisuus paljastuisi.
❌ Väärin:
<script>
import { generateSignedOgUrl } from "$lib/og-image-signer";
// This runs in the browser!
const ogUrl = generateSignedOgUrl({ title: post.title });
</script>
✅ Oikein:
// +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 };
}
Eri tavujen allekirjoittaminen kuin mitä lähetät
Allekirjoitus kattaa GET/api/v1/og sekä rivinvaihdon ja kanonisen kyselyn — lajitellut enc(key)=enc(value)-parit jokaisesta parametristä paitsi s, mukaan lukien key. Rakenna lopullinen URL samoista koodatuista pareista, tai palvelin palauttaa 401 invalid_signature.