Prompt condivisione Facebook
Premi il tasto qui sotto per copiare tutto, poi incollalo nella chat Base44 dell'altra app.
Copia tutto il prompt
Replica esattamente questa funzionalità nella mia app: pubblicazione automatica di un evento sulla pagina Facebook + condivisione del link con anteprima corretta (immagine 4:5 ad alta risoluzione, titolo, descrizione). SECRETS NECESSARI (impostali prima nei Settings → Environment Variables): - FACEBOOK_APP_ID - FACEBOOK_APP_SECRET - FACEBOOK_PAGE_ID - FACEBOOK_USER_ACCESS_TOKEN (token utente long-lived con scope: pages_manage_posts, pages_read_engagement, pages_show_list) - FACEBOOK_PAGE_ACCESS_TOKEN (opzionale, fallback) Sostituisci "https://villamaremonti.com" con il dominio della mia app in tutti e 3 i file. Sostituisci eventuali campi dell'entità Event (title, image_url, date, time, event_type, description, price_per_person) con i nomi dei campi della mia entità se diversi. ========================================== FILE 1: functions/publishFacebookEvent.js ========================================== Pubblica una FOTO sulla pagina Facebook con caption (titolo + data + descrizione + link prenotazione) e aggiunge un commento automatico con il link. ```js import { createClientFromRequest } from 'npm:@base44/sdk@0.8.25'; const toSlug = (title) => title?.toLowerCase() .replace(/[àáâãäå]/g, 'a').replace(/[èéêë]/g, 'e').replace(/[ìíîï]/g, 'i') .replace(/[òóôõö]/g, 'o').replace(/[ùúûü]/g, 'u').replace(/[ñ]/g, 'n') .replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').trim() || ''; const formatItalianDate = (dateString) => { if (!dateString) return ''; const date = new Date(dateString); const months = ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre']; return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`; }; const facebookApiVersion = 'v20.0'; const getAppAccessToken = () => { const appId = Deno.env.get('FACEBOOK_APP_ID'); const appSecret = Deno.env.get('FACEBOOK_APP_SECRET'); return appId && appSecret ? `${appId}|${appSecret}` : null; }; const debugFacebookToken = async (token) => { const appAccessToken = getAppAccessToken(); if (!appAccessToken) return null; const response = await fetch(`https://graph.facebook.com/${facebookApiVersion}/debug_token?input_token=${encodeURIComponent(token)}&access_token=${encodeURIComponent(appAccessToken)}`); const result = await response.json(); return response.ok ? result?.data : null; }; const exchangeLongLivedUserToken = async (userToken) => { const appId = Deno.env.get('FACEBOOK_APP_ID'); const appSecret = Deno.env.get('FACEBOOK_APP_SECRET'); if (!appId || !appSecret) return userToken; const response = await fetch(`https://graph.facebook.com/${facebookApiVersion}/oauth/access_token?grant_type=fb_exchange_token&client_id=${encodeURIComponent(appId)}&client_secret=${encodeURIComponent(appSecret)}&fb_exchange_token=${encodeURIComponent(userToken)}`); const result = await response.json(); return response.ok && result?.access_token ? result.access_token : userToken; }; const getPageTokenFromUserToken = async (pageId, userToken) => { const refreshedUserToken = await exchangeLongLivedUserToken(userToken); const accountsResponse = await fetch(`https://graph.facebook.com/${facebookApiVersion}/me/accounts?fields=id,name,access_token&access_token=${encodeURIComponent(refreshedUserToken)}`); const accountsResult = await accountsResponse.json(); if (!accountsResponse.ok) throw new Error(accountsResult?.error?.message || 'Impossibile recuperare le pagine FB.'); const page = (accountsResult.data || []).find((item) => item.id === pageId); if (!page?.access_token) throw new Error('Pagina FB non trovata o permessi insufficienti (servono pages_manage_posts, pages_read_engagement, pages_show_list).'); return page.access_token; }; const getValidPageToken = async (pageId) => { const userAccessToken = Deno.env.get('FACEBOOK_USER_ACCESS_TOKEN'); const savedPageToken = Deno.env.get('FACEBOOK_PAGE_ACCESS_TOKEN'); let candidateToken = null; let userTokenError = ''; if (userAccessToken) { try { candidateToken = await getPageTokenFromUserToken(pageId, userAccessToken); } catch (error) { userTokenError = error.message; } } if (!candidateToken && savedPageToken) candidateToken = savedPageToken; if (!candidateToken) throw new Error(userTokenError || 'Credenziali Facebook mancanti'); const tokenInfo = await debugFacebookToken(candidateToken); const scopes = tokenInfo?.scopes || []; if (scopes.includes('publish_actions')) throw new Error('Token vecchio con publish_actions. Rigenera con pages_manage_posts.'); if (tokenInfo && tokenInfo.is_valid === false) throw new Error('Token Facebook non valido. Rigenera il token utente long-lived.'); if (scopes.length > 0 && !scopes.includes('pages_manage_posts')) throw new Error('Token senza pages_manage_posts.'); return candidateToken; }; Deno.serve(async (req) => { try { const base44 = createClientFromRequest(req); const user = await base44.auth.me(); if (user?.role !== 'admin') return Response.json({ error: 'Forbidden' }, { status: 403 }); const { eventId } = await req.json(); if (!eventId) return Response.json({ error: 'Event ID mancante' }, { status: 400 }); const pageId = Deno.env.get('FACEBOOK_PAGE_ID'); const baseUrl = 'https://villamaremonti.com'; // ← SOSTITUISCI col tuo dominio if (!pageId) return Response.json({ error: 'FACEBOOK_PAGE_ID mancante' }, { status: 500 }); const event = await base44.asServiceRole.entities.Event.get(eventId); if (!event) return Response.json({ error: 'Evento non trovato' }, { status: 404 }); if (!event.image_url) return Response.json({ error: "Aggiungi prima un'immagine all'evento" }, { status: 400 }); const slug = `${toSlug(event.title)}-${event.id}`; const eventUrl = `${baseUrl}/eventi/${slug}`; const dateLine = event.date ? `📅 ${formatItalianDate(event.date)}${event.time ? ` alle ${event.time}` : ''}` : ''; const priceLine = event.price_per_person ? `💶 €${event.price_per_person} a persona` : ''; const caption = [ `✨ ${event.title}`, `👉 Prenota qui: ${eventUrl}`, '', dateLine, '📍 Villa Maremonti', priceLine, '', event.description || '', '', `Link iscrizione: ${eventUrl}`, ].filter(Boolean).join('\n'); const validPageToken = await getValidPageToken(pageId); const imageResponse = await fetch(event.image_url); if (!imageResponse.ok) return Response.json({ error: 'Immagine non raggiungibile' }, { status: 502 }); const imageBlob = new Blob([await imageResponse.arrayBuffer()], { type: imageResponse.headers.get('content-type') || 'image/jpeg', }); const formData = new FormData(); formData.append('source', imageBlob, 'evento.jpg'); formData.append('caption', caption); formData.append('access_token', validPageToken); const response = await fetch(`https://graph.facebook.com/v20.0/${pageId}/photos`, { method: 'POST', body: formData }); const result = await response.json(); if (!response.ok) return Response.json({ error: result?.error?.message || 'Pubblicazione fallita' }, { status: 502 }); const photoId = result.id; if (photoId) { const commentData = new FormData(); commentData.append('message', `Prenota qui: ${eventUrl}`); commentData.append('access_token', validPageToken); await fetch(`https://graph.facebook.com/v20.0/${photoId}/comments`, { method: 'POST', body: commentData }); } return Response.json({ success: true, postId: result.post_id || result.id }); } catch (error) { return Response.json({ error: error.message }, { status: 500 }); } }); ``` ========================================== FILE 2: functions/ogEvent.js ========================================== Genera la pagina HTML con i meta Open Graph che Facebook legge. Per gli utenti reali → redirect alla pagina evento; per i crawler social → HTML con i meta. ```js import { createClientFromRequest } from 'npm:@base44/sdk@0.8.25'; const extractId = (segment) => { const match = segment?.match(/([a-f0-9]{24})$/); return match ? match[1] : segment; }; const toSlug = (title) => title?.toLowerCase() .replace(/[àáâãäå]/g, 'a').replace(/[èéêë]/g, 'e').replace(/[ìíîï]/g, 'i') .replace(/[òóôõö]/g, 'o').replace(/[ùúûü]/g, 'u').replace(/[ñ]/g, 'n') .replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').trim() || ''; const esc = (s) => String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); const isSocialCrawler = (ua) => { const u = (ua || '').toLowerCase(); return /facebookexternalhit|facebot|whatsapp|twitterbot|linkedinbot|slackbot|telegrambot|discordbot|pinterest|googlebot|bingbot|skypeuripreview|embedly|redditbot|applebot/i.test(u); }; Deno.serve(async (req) => { try { const url = new URL(req.url); const rawParam = url.searchParams.get('slug') || url.searchParams.get('id') || ''; const eventId = extractId(rawParam); if (!eventId) return new Response('Missing id', { status: 400 }); const base44 = createClientFromRequest(req); const event = await base44.asServiceRole.entities.Event.get(eventId); if (!event) return new Response('Event not found', { status: 404 }); const appDomain = 'https://villamaremonti.com'; // ← SOSTITUISCI col tuo dominio const slug = `${toSlug(event.title)}-${event.id}`; const eventPageUrl = `${appDomain}/eventi/${slug}`; const shareVersion = 'fb-4x5-1440x1800-v3'; const proxyUrl = `${appDomain}/functions/ogEvent?slug=${encodeURIComponent(slug)}&v=${shareVersion}`; const userAgent = req.headers.get('user-agent') || ''; if (!isSocialCrawler(userAgent)) { return new Response(null, { status: 302, headers: { 'Location': eventPageUrl, 'Cache-Control': 'no-store' } }); } let dataFormattata = ''; if (event.date) { const d = new Date(event.date); const mesi = ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre']; dataFormattata = `${d.getDate()} ${mesi[d.getMonth()]} ${d.getFullYear()}`; } const orario = event.time ? ` alle ${event.time}` : ''; const ogDescription = [event.event_type, `${dataFormattata}${orario}`, 'Villa Maremonti'].filter(Boolean).join(' · '); const rawImageUrl = event.image_url || ''; const imageUrl = `${appDomain}/functions/ogImage?url=${encodeURIComponent(rawImageUrl)}&format=4x5&w=1440&h=1800&v=${shareVersion}`; const html = `<!DOCTYPE html> <html prefix="og: https://ogp.me/ns#" lang="it"> <head> <meta charset="utf-8" /> <title>${esc(event.title)}</title> <meta name="description" content="${esc(ogDescription)}" /> <meta property="og:site_name" content="Villa Maremonti" /> <meta property="og:type" content="website" /> <meta property="og:url" content="${esc(proxyUrl)}" /> <meta property="og:title" content="${esc(event.title)}" /> <meta property="og:description" content="${esc(ogDescription)}" /> <meta property="og:image" content="${esc(imageUrl)}" /> <meta property="og:image:secure_url" content="${esc(imageUrl)}" /> <meta property="og:image:width" content="1440" /> <meta property="og:image:height" content="1800" /> <meta property="og:image:type" content="image/jpeg" /> <meta property="og:locale" content="it_IT" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:image" content="${esc(imageUrl)}" /> <link rel="canonical" href="${esc(eventPageUrl)}" /> </head> <body><h1>${esc(event.title)}</h1><p><a href="${esc(eventPageUrl)}">Apri l'evento</a></p></body> </html>`; return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' } }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ========================================== FILE 3: functions/ogImage.js ========================================== Riquadra qualsiasi immagine in 4:5 (1440x1800) ad alta qualità — formato preferito da Facebook. ```js import Jimp from 'npm:jimp@0.22.12'; import { Buffer } from 'node:buffer'; Deno.serve(async (req) => { try { const url = new URL(req.url); const imageUrl = url.searchParams.get('url'); const targetWidth = parseInt(url.searchParams.get('w') || '1440', 10); const targetHeight = parseInt(url.searchParams.get('h') || '1800', 10); if (!imageUrl) return new Response('Missing url', { status: 400 }); const response = await fetch(imageUrl, { headers: { 'User-Agent': 'facebookexternalhit/1.1', 'Accept': 'image/jpeg,image/png,image/webp,image/*' }, }); if (!response.ok) return new Response('Image fetch failed', { status: 502 }); const inputBuffer = Buffer.from(await response.arrayBuffer()); const image = await Jimp.read(inputBuffer); image.cover(targetWidth, targetHeight).quality(92); const outputBuffer = await image.getBufferAsync(Jimp.MIME_JPEG); return new Response(outputBuffer, { status: 200, headers: { 'Content-Type': 'image/jpeg', 'Content-Length': String(outputBuffer.length), 'Cache-Control': 'public, max-age=31536000, immutable', 'Access-Control-Allow-Origin': '*', }, }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ========================================== USO DAL FRONTEND ========================================== Bottone "Pubblica su Facebook" nell'admin: ```jsx import { base44 } from '@/api/base44Client'; const handlePublishFB = async (eventId) => { try { const res = await base44.functions.invoke('publishFacebookEvent', { eventId }); alert('Pubblicato! Post ID: ' + res.data.postId); } catch (err) { alert('Errore: ' + (err?.response?.data?.error || err.message)); } }; ``` Bottone "Condividi su Facebook" — usa ogEvent come URL così Facebook legge i meta: ```jsx const handleShareFacebook = () => { const slug = `${toSlug(event.title)}-${event.id}`; const previewUrl = `https://villamaremonti.com/functions/ogEvent?slug=${encodeURIComponent(slug)}&v=fb-4x5-1440x1800-v3`; window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(previewUrl)}`, '_blank'); }; ``` ========================================== PERCHÉ FUNZIONA ========================================== 1. Il token utente long-lived viene scambiato automaticamente e da lì si ricava il Page Token (non scade dopo 60 giorni) 2. La pubblicazione usa /photos con FormData (binario) — non URL — così FB accetta sempre l'immagine 3. ogEvent distingue crawler vs utenti: i crawler ricevono HTML con meta OG, gli utenti redirect alla pagina evento 4. ogImage riquadra in 4:5 ad alta risoluzione — formato che FB mostra grande senza tagli 5. og:url punta a ogEvent stesso (non alla SPA) per evitare ricrawl che leggerebbero meta vuoti Dopo il deploy testa qui: https://developers.facebook.com/tools/debug/ → incolla l'URL ogEvent → "Scrape Again".