Emne

OG-Bildgenerering for React-apper

Generer og:image med React ved hjelp av FastOG-API-et — med et Dynamic API-abonnement (opprett API-nøkkelen din i dashbordet), abonnementsbasert, HMAC-sikret. Denne guiden viser deg hvordan du lager et signeringsverktøy for HMAC-signerte OG-bildelenker og gjengir dynamiske Open Graph-bilder på farten. Prøv den gratis OG-bildetesteren for å forhåndsvise designet ditt før du lanserer.

Hurtigstart

1. Opprett signeringsverktøyet

javascript
// src/utils/og-image-signer.js
export async function generateSignedOgUrl(params, options = {}) {
    const {
        baseUrl = "https://fastog.com/api/v1/og",
        apiKey = import.meta.env.VITE_FASTOG_API_KEY ||
            process.env.REACT_APP_FASTOG_API_KEY,
        hmacSecret = import.meta.env.VITE_FASTOG_HMAC_SECRET ||
            process.env.REACT_APP_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 (Web Crypto HMAC-SHA256)
    const signingKey = await crypto.subtle.importKey(
        "raw",
        new TextEncoder().encode(hmacSecret),
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"],
    );
    const signature = Array.from(
        new Uint8Array(
            await crypto.subtle.sign(
                "HMAC",
                signingKey,
                new TextEncoder().encode(`GET/api/v1/og\n${canonical}`),
            ),
        ),
    )
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");

    // 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();
}

⚠️ Viktig: I React-SPA-er skjer signeringen på klientsiden. For produksjon bør du flytte signeringen til en backend-API-rute for å holde HMAC-hemmeligheten sikker.

Express-backend

javascript
// server/routes/og.js
import express from "express";
import crypto from "crypto";

const router = express.Router();

router.get("/api/og", (req, res) => {
    const { title, template = "default", ...params } = req.query;

    if (!title) {
        return res.status(400).json({ error: "Missing title" });
    }

    const apiKey = process.env.FASTOG_API_KEY;
    const hmacSecret = process.env.FASTOG_HMAC_SECRET;

    const enc = (s) =>
        encodeURIComponent(s).replace(
            /[!'()*]/g,
            (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
        );

    const all = { template, title, ...params, key: apiKey };

    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("&");

    const signature = crypto
        .createHmac("sha256", hmacSecret)
        .update(`GET/api/v1/og\n${canonical}`)
        .digest("hex");

    const url = new URL("https://fastog.com/api/v1/og");
    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);

    res.json({ ogImageUrl: url.toString() });
});

export default router;

React-komponent

jsx
// src/components/OgImage.jsx
import { useState, useEffect } from "react";

export function useOgImage(params) {
    const [ogImageUrl, setOgImageUrl] = useState(null);

    useEffect(() => {
        const searchParams = new URLSearchParams(params);
        fetch(`/api/og?${searchParams}`)
            .then((res) => res.json())
            .then((data) => setOgImageUrl(data.ogImageUrl))
            .catch((err) => console.error("Failed to generate OG image:", err));
    }, [JSON.stringify(params)]);

    return ogImageUrl;
}

Eksempel på blogginnlegg

jsx
// src/pages/BlogPost.jsx
import { useEffect, useState } from "react";
import { useOgImage } from "../components/OgImage";

export default function BlogPost({ post }) {
    const ogImageUrl = useOgImage({
        title: post.title,
        template: "blog",
        subtitle: post.excerpt,
    });

    useEffect(() => {
        const ogImage = document.querySelector('meta[property="og:image"]');
        if (ogImage && ogImageUrl) {
            ogImage.setAttribute("content", ogImageUrl);
        }
    }, [ogImageUrl]);

    return (
        <article>
            <h1>{post.title}</h1>
            <p>{post.excerpt}</p>
        </article>
    );
}

React Helmet (SSR)

jsx
// src/pages/BlogPost.jsx
import { Helmet } from "react-helmet-async";
import { useOgImage } from "../components/OgImage";

export default function BlogPost({ post }) {
    const ogImageUrl = useOgImage({
        title: post.title,
        template: "blog",
        subtitle: post.excerpt,
    });

    return (
        <>
            <Helmet>
                <meta
                    property="og:image"
                    content={ogImageUrl || "/fallback-og.png"}
                />
                <meta property="og:title" content={post.title} />
                <meta property="og:description" content={post.excerpt} />
            </Helmet>
            <article>
                <h1>{post.title}</h1>
            </article>
        </>
    );
}

Next.js (React-rammeverk)

For Next.js, se de dedikerte veiledningene for Next.js App Router og Next.js Pages Router.

Remix

jsx
// app/routes/blog.$slug.jsx
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { generateSignedOgUrl } from "~/lib/og-image-signer.server";

export async function loader({ params }) {
    const post = await getPost(params.slug);

    const ogImageUrl = generateSignedOgUrl({
        template: "blog",
        title: post.title,
        subtitle: post.excerpt,
    });

    return json({ post, ogImageUrl });
}

export function meta({ data }) {
    return [
        { title: data.post.title },
        { property: "og:image", content: data.ogImageUrl },
        { property: "og:title", content: data.post.title },
    ];
}

export default function BlogPost() {
    const { post } = useLoaderData();

    return (
        <article>
            <h1>{post.title}</h1>
        </article>
    );
}

Tjenerside-signer (Node.js)

javascript
// lib/og-image-signer.server.js
import crypto from "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;

    // 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();
}

Miljøvariabler

bash
# .env (Vite)
VITE_FASTOG_API_KEY=sk_your_full_api_key_here
VITE_FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

# .env (Create React App)
REACT_APP_FASTOG_API_KEY=sk_your_full_api_key_here
REACT_APP_FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

# Server .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

Vanlige fallgruver

Eksponering av HMAC-hemmeligheten i klientkode

❌ Feil:

javascript
// In browser code
const ogUrl = generateSignedOgUrl(params, {
    hmacSecret: import.meta.env.VITE_FASTOG_HMAC_SECRET,
});

✅ Riktig:

javascript
// Call your backend API
const res = await fetch(`/api/og?title=${post.title}`);
const { ogImageUrl } = await res.json();

Oppdaterer ikke meta-tagger

Sosiale crawlers trenger meta-tagger i den opprinnelige HTML-en. Bruk SSR (Next.js, Remix) eller en backend-proxy for riktig OG-bildestøtte.

Full API-referanse

Prøv gratis – ingen registrering nødvendig

Forhåndsvis og test dynamiske OG-bilder på sekunder.

Åpne den gratis OG-bildetesteren

A BeCaSimulations site