صور OG في Vue وNuxt
قم بإنشاء صور OG في Vue وNuxt باستخدام واجهة برمجة تطبيقات FastOG — مع اشتراك Dynamic API (أنشئ مفتاح API الخاص بك في لوحة التحكم)، قائم على الاشتراك، ومؤمَّن عبر HMAC. يوضح هذا الدليل كيفية إنشاء أداة توقيع لعناوين URL لصور OG الموقَّعة بـ HMAC وعرض صور Open Graph الديناميكية أثناء الطلب. جرّب الصورة باستخدام أداة اختبار OG المجانية قبل النشر.
بدء سريع
1. إنشاء أداة التوقيع (signer utility)
// 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. متغيرات البيئة
# .env
FASTOG_API_KEY=sk_your_full_api_key_here
FASTOG_HMAC_SECRET=your_64_char_hex_secret_here
Nuxt 3
صفحة باستخدام useHead
<!-- 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 من جهة الخادم
// 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
// 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;
}
المكوّن
<!-- 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>
واجهة برمجة التطبيقات الخلفية (Express)
// 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");
});
مثال التجارة الإلكترونية
<!-- 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.
<!-- 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 في كود المتصفح.
❌ خطأ:
<script setup>
import { generateSignedOgUrl } from "~/lib/og-image-signer";
// Runs in browser — secret exposed!
const ogUrl = generateSignedOgUrl({ title: post.title });
</script>
✅ صحيح:
// 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 بالإضافة إلى سطر جديد والاستعلام الأساسي — أزواج enc(key)=enc(value) المرتبة لكل معلمة باستثناء s، بما في ذلك key. أنشئ الرابط النهائي من نفس الأزواج المشفرة، وإلا سيعيد الخادم 401 invalid_signature.