หัวข้อ

OG Images ใน Vue และ Nuxt

สร้าง OG images ใน Vue และ Nuxt ด้วย FastOG API — พร้อมการสมัครสมาชิก Dynamic API (สร้าง API key ของคุณในแดชบอร์ด), แบบอิงการสมัครสมาชิก, และรักษาความปลอดภัยด้วย HMAC คู่มือนี้แสดงวิธีสร้างยูทิลิตี้ตัวลงนาม (signer utility) สำหรับ URL ของ OG images ที่ลงนามด้วย HMAC และแสดงผล Open Graph images แบบไดนามิกได้ทันที ทดสอบล่วงหน้าด้วย Free OG Image Tester ก่อนเผยแพร่

เริ่มต้นใช้งานอย่างรวดเร็ว

1. สร้างยูทิลิตี้สำหรับผู้ลงนาม (signer utility)

javascript
// lib/og-image-signer.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;

    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. ตัวแปรสภาพแวดล้อม (environment variables)

bash
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here

Nuxt 3

เพจที่ใช้ useHead

vue
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute();
const { data: post } = await useAsyncData("post", () =>
    getPost(route.params.slug),
);

const { data: ogImageUrl } = await useAsyncData("og-image", () =>
    $fetch("/api/og", {
        query: {
            title: post.value.title,
            template: "blog",
            subtitle: post.value.excerpt,
        },
    }).then((res) => res.ogImageUrl),
);

useHead({
    title: post.value.title,
    meta: [
        { property: "og:image", content: ogImageUrl.value },
        { property: "og:title", content: post.value.title },
        { property: "og:description", content: post.value.excerpt },
    ],
});
</script>

<template>
    <article>
        <h1>{{ post.title }}</h1>
        <p>{{ post.excerpt }}</p>
    </article>
</template>

เส้นทาง API ฝั่งเซิร์ฟเวอร์

javascript
// server/api/og.get.js
import { generateSignedOgUrl } from "~/lib/og-image-signer";

export default defineEventHandler(async (event) => {
    const query = getQuery(event);
    const { title, template = "default", ...params } = query;

    if (!title) {
        throw createError({ statusCode: 400, message: "Missing title" });
    }

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

    return { ogImageUrl };
});

Vue 3 (SPA)

Composable

javascript
// composables/useOgImage.js
import { ref, watch } from "vue";

export function useOgImage(params) {
    const ogImageUrl = ref(null);

    watch(
        () => params,
        async (newParams) => {
            try {
                const searchParams = new URLSearchParams(newParams);
                const res = await fetch(`/api/og?${searchParams}`);
                const data = await res.json();
                ogImageUrl.value = data.ogImageUrl;
            } catch (err) {
                console.error("Failed to generate OG image:", err);
            }
        },
        { immediate: true },
    );

    return ogImageUrl;
}

คอมโพเนนต์

vue
<!-- components/BlogPost.vue -->
<script setup>
import { useOgImage } from "~/composables/useOgImage";

const props = defineProps({
    post: Object,
});

const ogImageUrl = useOgImage({
    title: props.post.title,
    template: "blog",
    subtitle: props.post.excerpt,
});
</script>

<template>
    <article>
        <h1>{{ post.title }}</h1>
        <p>{{ post.excerpt }}</p>
    </article>
</template>

Backend API (Express)

javascript
// server/index.js
import express from "express";
import crypto from "crypto";

const app = express();

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

app.listen(3001, () => {
    console.log("API server running on port 3001");
});

ตัวอย่างอีคอมเมิร์ซ

vue
<!-- pages/products/[id].vue -->
<script setup>
const route = useRoute();
const { data: product } = await useAsyncData("product", () =>
    getProduct(route.params.id),
);

const { data: ogImageUrl } = await useAsyncData("og-image", () =>
    $fetch("/api/og", {
        query: {
            title: product.value.name,
            template: "ecommerce",
            price: product.value.price,
            image: product.value.image,
        },
    }).then((res) => res.ogImageUrl),
);

useHead({
    meta: [
        { property: "og:image", content: ogImageUrl.value },
        { property: "og:title", content: product.value.name },
    ],
});
</script>

<template>
    <main>
        <h1>{{ product.name }}</h1>
        <p>{{ product.price }}</p>
    </main>
</template>

โปรไฟล์ผู้ใช้ (เทมเพลตบล็อก)

FastOG ไม่มีเทมเพลต profile — ให้ใช้ blog สำหรับการ์ดสไตล์โปรไฟล์ โดยส่งชื่อผู้ใช้เป็น title, ประวัติย่อเป็น subtitle และชื่อเป็น author

vue
<!-- pages/users/[username].vue -->
<script setup>
const route = useRoute();
const { data: user } = await useAsyncData("user", () =>
    getUser(route.params.username),
);

const { data: ogImageUrl } = await useAsyncData("og-image", () =>
    $fetch("/api/og", {
        query: {
            title: `${user.value.name} - Profile`,
            template: "blog",
            subtitle: user.value.bio,
            author: user.value.name,
        },
    }).then((res) => res.ogImageUrl),
);

useHead({
    meta: [
        { property: "og:image", content: ogImageUrl.value },
        { property: "og:title", content: `${user.value.name} - Profile` },
    ],
});
</script>

ข้อผิดพลาดที่พบบ่อย

การเซ็นชื่อฝั่งไคลเอนต์

ห้ามเซ็นชื่อ OG URL ในโค้ดที่ทำงานบนเบราว์เซอร์โดยเด็ดขาด

❌ ผิด:

vue
<script setup>
import { generateSignedOgUrl } from "~/lib/og-image-signer";
// Runs in browser — secret exposed!
const ogUrl = generateSignedOgUrl({ title: post.title });
</script>

✅ ถูกต้อง:

javascript
// server/api/og.get.js (runs on server only)
import { generateSignedOgUrl } from "~/lib/og-image-signer";
export default defineEventHandler((event) => {
    return { ogImageUrl: generateSignedOgUrl({ title: query.title }) };
});

การเซ็นชื่อไบต์ที่แตกต่างจากที่ส่งไป

ลายเซ็นครอบคลุม GET/api/v1/og บวกกับอักขระขึ้นบรรทัดใหม่ และคิวรีแบบ canonical ซึ่งก็คือคู่ enc(key)=enc(value) ที่เรียงลำดับแล้วของทุกพารามิเตอร์ ยกเว้น s รวมถึง key ด้วย สร้าง URL สุดท้ายจากคู่ที่เข้ารหัสชุดเดียวกันนี้ มิฉะนั้นเซิร์ฟเวอร์จะคืนค่า 401 invalid_signature

เอกสารอ้างอิง API ฉบับเต็ม

ลองใช้ฟรี — ไม่ต้องสมัครสมาชิก

ดูตัวอย่างและทดสอบภาพ OG แบบไดนามิกได้ในไม่กี่วินาที

เปิดตัวทดสอบภาพ OG ฟรี

A BeCaSimulations site