חזרה לגלריהכללי

יצירת קישורים שיתופיים בווטאספ

רוצים לשתף קישורים בתוך האתר וכל קישור יציג תמונה ותיאור שונה? הפרופמט הבא ידע לפתור את זה

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: & < > " ' → &amp; &lt; &gt; &quot; &#039;

**קוד לדוגמה:**
```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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;");
}
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 בשמות השדות שלו.