חזרה לגלריהכללי
יצירת קישורים שיתופיים בווטאספ
רוצים לשתף קישורים בתוך האתר וכל קישור יציג תמונה ותיאור שונה? הפרופמט הבא ידע לפתור את זה
D
Daniel Rechtemanטקסט הפרומפט
פרומפט גנרי: בניית מערכת שיתוף OG (Open Graph) לכל Entity ב-Base44
אני רוצה לבנות מערכת שיתוף עם תמיכה ב-Open Graph (OG tags) עבור אפליקציית Base44.
המערכת מאפשרת שיתוף קישורים ברשתות חברתיות (וואטסאפ, פייסבוק, טוויטר וכו') עם פריוויו עשיר — כולל כותרת, תיאור ותמונה.
המערכת צריכה להיות גנרית ולתמוך בכל סוג Entity.
---
## מה המשתמש צריך לספק:
1. **שם ה-Entity** — למשל: MiniSite, Product, Listing, Event, Article וכו'
2. **שדה מזהה ייחודי ב-URL (slug)** — שם השדה שמשמש כמזהה ייחודי בקישור הציבורי (למשל: slug, url_id, handle)
3. **שדה כותרת** — שם השדה שמשמש ככותרת ב-OG (למשל: title, name, property_name)
4. **שדה תיאור** — שם השדה לתיאור (למשל: description, short_description, summary)
5. **שדה תמונה** — שם השדה ל-URL של תמונה (למשל: image, hero_image, cover_photo, thumbnail)
6. **שם האתר** — שם הברנד שיופיע ב-og:site_name (למשל: "MyApp", "MiniSite.AI")
7. **שדה סטטוס פרסום (אופציונלי)** — אם יש שדה שמציין אם הרשומה פורסמה (למשל: status === "published", is_active === true). אם אין — כל הרשומות נחשבות ציבוריות.
8. **שדה חסימה (אופציונלי)** — אם יש שדה שמציין שהרשומה חסומה (למשל: is_blocked). אם אין — אף רשומה לא חסומה.
9. **שפה וכיוון** — האם האתר בעברית (RTL) או באנגלית (LTR)?
10. **APP_URL** — הדומיין המלא של האפליקציה (למשל: https://myapp.com או https://myapp.base44.app)
---
## ארכיטקטורת המערכת:
### Secret נדרש:
- APP_URL — כתובת הדומיין של האפליקציה (יוגדר ב-Dashboard → Settings → Secrets)
- BASE44_APP_ID — קיים אוטומטית בכל אפליקציית Base44
---
### 1. Backend Function: ogRedirect
פונקציית Deno שמטפלת בקישורי שיתוף.
**איך היא עובדת:**
- מקבלת את ה-slug מ-query parameter: ?s={slug}
- קוראת ישירות מה-DB דרך asServiceRole (עוקף RLS)
- מסננת רק רשומות שפורסמו ולא חסומות (אם רלוונטי)
- בונה HTML סטטי עם תגיות OG — קרולרים של רשתות חברתיות קוראים את זה
- משתמשים אנושיים מופנים אוטומטית לעמוד ה-SPA דרך JavaScript
**זיהוי הדומיין להפניה (סדר עדיפויות):**
1. x-forwarded-host header — כשהבקשה עוברת דרך ה-proxy של Base44
2. APP_URL — מ-environment variable
3. Fallback: https://app.base44.app/apps/{APP_ID}
**דגשים טכניים:**
- מחזירה status 200 (לא redirect) — כי קרולרים חייבים לקרוא את ה-HTML
- הפניית המשתמש האנושי נעשית ב-JavaScript עם setTimeout(100ms)
- Cache-Control: public, max-age=3600 (שעה) כדי לא להעמיס
- תמונות שלא מתחילות ב-http מקבלות prefix של https://base44.app
- יש תמונת fallback למקרה שאין תמונה ברשומה
- HTML כולל גם לינק ידני "לחצו כאן" למקרה שה-JS redirect לא עובד
**תגיות OG שנבנות:**
- og:title — {כותרת} | {שם האתר}
- og:description — עד 200 תווים מהתיאור
- og:image + og:image:secure_url — תמונה ראשית או fallback
- og:image:width: 1200, og:image:height: 630
- og:url — הקישור הישיר לעמוד (/p?s={slug})
- og:site_name — שם הברנד
- og:locale — לפי השפה (he_IL או en_US)
- og:type — website
- twitter:card — summary_large_image
- twitter:title, twitter:description, twitter:image — זהה ל-OG
**Escaping:**
כל ערכים ב-HTML חייבים לעבור escape: & < > " ' → & < > " '
**קוד לדוגמה:**
```javascript
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.20';
Deno.serve(async (req) => {
try {
const url = new URL(req.url);
let slug = url.searchParams.get("s") || url.searchParams.get("slug");
// Fallback: POST body (לטסטים)
if (!slug && req.method === "POST") {
try { const body = await req.json(); slug = body.s || body.slug || null; } catch {}
}
if (!slug) {
return new Response("Missing slug", { status: 400 });
}
// זיהוי דומיין
const forwardedHost = req.headers.get("x-forwarded-host");
const forwardedProto = req.headers.get("x-forwarded-proto") || "https";
let appOrigin;
if (forwardedHost) {
appOrigin = `${forwardedProto}://${forwardedHost}`;
} else {
const envUrl = Deno.env.get("APP_URL");
appOrigin = envUrl ? envUrl.replace(/\/+$/, "") : `https://app.base44.app/apps/${Deno.env.get("BASE44_APP_ID")}`;
}
const targetUrl = `${appOrigin}/p?s=${encodeURIComponent(slug)}`;
// קריאה מה-DB דרך service role
let record = null;
try {
const base44 = createClientFromRequest(req);
const records = await base44.asServiceRole.entities.{ENTITY_NAME}.filter({ {SLUG_FIELD}: slug });
record = (records || []).find(r =>
(!{HAS_STATUS_FIELD} || r.{STATUS_FIELD} === "{PUBLISHED_VALUE}") &&
(!{HAS_BLOCKED_FIELD} || !r.{BLOCKED_FIELD})
);
} catch (err) {
console.error("[ogRedirect] DB error:", err.message);
}
if (!record) {
return Response.redirect(targetUrl, 302);
}
// בניית OG
const siteName = "{SITE_NAME}";
const title = record.{TITLE_FIELD} ? `${record.{TITLE_FIELD}} | ${siteName}` : siteName;
const description = (record.{DESCRIPTION_FIELD} || "").slice(0, 200);
let imageUrl = record.{IMAGE_FIELD} || "";
if (imageUrl && !imageUrl.startsWith("http")) imageUrl = "https://base44.app" + imageUrl;
const ogImage = imageUrl || "{FALLBACK_IMAGE_URL}";
const html = `<!DOCTYPE html>
<html lang="{LANG}" dir="{DIR}">
<head>
<meta charset="UTF-8">
<title>${esc(title)}</title>
<meta name="description" content="${esc(description)}">
<meta property="og:type" content="website">
<meta property="og:url" content="${esc(targetUrl)}">
<meta property="og:title" content="${esc(title)}">
<meta property="og:description" content="${esc(description)}">
<meta property="og:image" content="${esc(ogImage)}">
<meta property="og:image:secure_url" content="${esc(ogImage)}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="${esc(siteName)}">
<meta property="og:locale" content="{LOCALE}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${esc(title)}">
<meta name="twitter:description" content="${esc(description)}">
<meta name="twitter:image" content="${esc(ogImage)}">
</head>
<body style="font-family:sans-serif;text-align:center;padding:40px;direction:{DIR}">
<p style="color:#666">מעביר לדף...</p>
<a href__="${esc(targetUrl)}" style="color:#007bff;text-decoration:underline">לחצו כאן</a>
<script>setTimeout(function(){window.location.replace("${targetUrl.replace(/"/g, '\\"')}")},100);</script>
</body>
</html>`;
return new Response(html, {
status: 200,
headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "public, max-age=3600" },
});
} catch (error) {
return new Response("Server error", { status: 500 });
}
});
function esc(str) {
if (!str) return "";
return String(str).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'");
}
Placeholders להחליף:
{ENTITY_NAME} — שם ה-Entity
{SLUG_FIELD} — שדה ה-slug
{TITLE_FIELD} — שדה הכותרת
{DESCRIPTION_FIELD} — שדה התיאור
{IMAGE_FIELD} — שדה התמונה
{STATUS_FIELD} — שדה הסטטוס (אם קיים)
{PUBLISHED_VALUE} — הערך שמציין "פורסם" (אם קיים)
{HAS_STATUS_FIELD} — true/false
{BLOCKED_FIELD} — שדה חסימה (אם קיים)
{HAS_BLOCKED_FIELD} — true/false
{SITE_NAME} — שם האתר
{FALLBACK_IMAGE_URL} — URL לתמונת ברירת מחדל
{LANG} — "he" או "en"
{DIR} — "rtl" או "ltr"
{LOCALE} — "he_IL" או "en_US"
2. Backend Function: getPublicRecord
פונקציה שמחזירה את הנתונים המלאים של רשומה ציבורית (לעמוד ה-SPA).
import { createClientFromRequest } from 'npm:@base44/sdk@0.8.20';
Deno.serve(async (req) => {
try {
const base44 = createClientFromRequest(req);
const body = await req.json();
const slug = body.slug;
if (!slug) return Response.json({ error: 'Missing slug' }, { status: 400 });
const records = await base44.asServiceRole.entities.{ENTITY_NAME}.filter({ {SLUG_FIELD}: slug });
const record = (records || []).find(r =>
(!{HAS_STATUS_FIELD} || r.{STATUS_FIELD} === "{PUBLISHED_VALUE}") &&
(!{HAS_BLOCKED_FIELD} || !r.{BLOCKED_FIELD})
);
if (!record) return Response.json({ error: 'Not found' }, { status: 404 });
return Response.json(record);
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
});
3. Frontend Utility: shareUrl.js
export function getShareUrl(slug) {
if (!slug) return "";
return `${window.location.origin}/functions/ogRedirect?s=${encodeURIComponent(slug)}`;
}
export function getDirectUrl(slug) {
if (!slug) return "";
return `${window.location.origin}/p?s=${encodeURIComponent(slug)}`;
}
getShareUrl — לשיתוף ברשתות חברתיות (עובר דרך ogRedirect → מקבל OG tags)
getDirectUrl — לפתיחה ישירה בדפדפן (ישר ל-SPA)
4. Frontend Page: /p (עמוד ציבורי)
עמוד React שמציג רשומה ציבורית:
מחלץ slug מ-URL: ?s={slug}
קורא ל-getPublicRecord דרך base44.functions.invoke
מציג את התוכן ברכיב renderer מתאים
עוקב אחרי אנליטיקס (אופציונלי)
חשוב: העמוד חייב להיות מוגדר כ-public בלייאאוט (ללא דרישת login).
5. זרימת השיתוף המלאה:
משתמש לוחץ "שתף"
↓
getShareUrl("my-slug")
↓
https://myapp.com/functions/ogRedirect?s=my-slug
↓
קרולר של רשת חברתית (וואטסאפ/פייסבוק) → קורא HTML → מציג פריוויו עשיר
↓
משתמש אנושי לוחץ על הלינק
↓
ogRedirect מחזיר HTML → JavaScript redirect תוך 100ms
↓
https://myapp.com/p?s=my-slug
↓
עמוד /p טוען → קורא getPublicRecord → מציג את התוכן
6. דגשים קריטיים:
RLS: אם ל-Entity יש RLS — חובה להשתמש ב-asServiceRole לגישה ציבורית
Status 200: ogRedirect חייב להחזיר 200, לא 301/302, כי קרולרים צריכים את ה-HTML
APP_URL: חובה להגדיר כ-secret כדי שההפניה תעבוד נכון
Escape: כל טקסט ב-HTML חייב escape כדי למנוע XSS
Cache: שעה של cache מספיקה — מאזנת בין ביצועים לעדכניות
תמונות: og:image חייב להיות URL מלא שמתחיל ב-https
POST fallback: הפונקציה תומכת גם ב-POST body לצורך טסטים מהדשבורד
---
זה פרומפט גנרי שעובד לכל Entity — המשתמש רק צריך להחליף את ה-placeholders בשמות השדות שלו.