Тема

OG-зображення в Astro

Генеруйте OG-зображення в Astro за допомогою FastOG API — з підпискою Dynamic API (створіть ваш API-ключ у панелі керування), на основі підписки, захищеної HMAC. Цей посібник показує, як створити утиліту підписувача для HMAC-підписаних URL-адрес OG-зображень і динамічно рендерити Open Graph-зображення на льоту. Перегляньте безкоштовний тестер OG-зображень, щоб попередньо переглянути результат перед інтеграцією.

Швидкий старт

1. Створіть утиліту підписувача (signer)

javascript
// 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 = import.meta.env.FASTOG_API_KEY,
        hmacSecret = import.meta.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. Змінні середовища

bash
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

Допис у блозі

astro
---
// src/pages/blog/[slug].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";

const post = await getPost(Astro.params.slug);

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

<html>
    <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} />
    </head>
    <body>
        <article>
            <h1>{post.title}</h1>
            <p>{post.excerpt}</p>
        </article>
    </body>
</html>

Колекції контенту

javascript
// src/content/config.ts
import { defineCollection, z } from "astro:content";

const blog = defineCollection({
    type: "content",
    schema: z.object({
        title: z.string(),
        excerpt: z.string(),
        author: z.string(),
        date: z.date(),
    }),
});

export const collections = { blog };
astro
---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";

const post = (await getCollection("blog")).find(
    (entry) => entry.slug === Astro.params.slug
);

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

<html>
    <head>
        <title>{post.data.title}</title>
        <meta property="og:image" content={ogImageUrl} />
        <meta property="og:title" content={post.data.title} />
        <meta property="og:description" content={post.data.excerpt} />
    </head>
    <body>
        <article>
            <h1>{post.data.title}</h1>
            <p>{post.data.excerpt}</p>
        </article>
    </body>
</html>

Приклад електронної комерції

astro
---
// src/pages/products/[id].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";

const product = await getProduct(Astro.params.id);

const ogImageUrl = generateSignedOgUrl({
    template: "ecommerce",
    title: product.name,
    price: product.price,
    image: product.image,
});
---

<html>
    <head>
        <meta property="og:image" content={ogImageUrl} />
        <meta property="og:title" content={product.name} />
    </head>
    <body>
        <h1>{product.name}</h1>
        <p>{product.price}</p>
    </body>
</html>

Профіль користувача (шаблон блогу)

У FastOG немає шаблону profile — використовуйте blog для картки в стилі профілю, передаючи ім’я користувача як title, біографію як subtitle та ім’я як author.

astro
---
// src/pages/users/[username].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";

const user = await getUser(Astro.params.username);

const ogImageUrl = generateSignedOgUrl({
    template: "blog",
    title: user.name,
    subtitle: user.bio,
    author: user.name,
});
---

<html>
    <head>
        <meta property="og:image" content={ogImageUrl} />
        <meta property="og:title" content={`${user.name} - Profile`} />
    </head>
    <body>
        <h1>{user.name}</h1>
        <p>{user.bio}</p>
    </body>
</html>

Кінцева точка API

javascript
// src/pages/api/og.js
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";

export async function GET({ request }) {
    const url = new URL(request.url);
    const title = url.searchParams.get("title");
    const template = url.searchParams.get("template") || "default";

    if (!title) {
        return new Response(JSON.stringify({ error: "Missing title" }), {
            status: 400,
            headers: { "Content-Type": "application/json" },
        });
    }

    const ogImageUrl = generateSignedOgUrl({
        template,
        title,
    });

    return new Response(JSON.stringify({ ogImageUrl }), {
        status: 200,
        headers: { "Content-Type": "application/json" },
    });
}

Компоненти фреймворків

Astro підтримує React, Vue, Svelte та інші. Підпис завжди відбувається у фронтматтері .astro (на стороні сервера):

astro
---
// src/pages/blog/[slug].astro
import { generateSignedOgUrl } from "../../lib/og-image-signer.js";
import BlogPost from "../../components/BlogPost.svelte";

const post = await getPost(Astro.params.slug);
const ogImageUrl = generateSignedOgUrl({
    template: "blog",
    title: post.title,
});
---

<html>
    <head>
        <meta property="og:image" content={ogImageUrl} />
    </head>
    <body>
        <BlogPost client:load {post} />
    </body>
</html>

Типові помилки

Підписання на стороні клієнта

Ніколи не підписуйте OG-URL у JavaScript на стороні клієнта.

❌ Неправильно:

astro
<script>
    // Runs in browser — secret exposed!
    const ogUrl = generateSignedOgUrl({ title: "Test" });
</script>

✅ Правильно:

astro
---
// Runs at build/SSR time — secret safe
const ogImageUrl = generateSignedOgUrl({ title: post.title });
---

Підписання інших байтів, ніж ви надсилаєте

Підпис покриває GET/api/v1/og плюс символ нового рядка та канонічний запит — відсортовані пари enc(key)=enc(value) для кожного параметра, окрім s, включно з key. Формуйте кінцевий URL із тих самих закодованих пар, інакше сервер поверне 401 invalid_signature.

Повна довідка API

Спробуйте безкоштовно — реєстрація не потрібна

Переглядайте та тестуйте динамічні OG-зображення за лічені секунди.

Відкрити безкоштовний тестер OG-зображень

A BeCaSimulations site