Skip to content

Commit 8843d99

Browse files
Add auto featured image
Implemented automatic featured image assignment for blog drafts in write_blog_post flow: - Try Unsplash search by title/topic to fetch a cover image - Fallback to Gemini-based image generation via Lovable gateway - Upload generated image to Supabase storage and attach as featured_image with alt text - Persist featured_image fields on blog_posts insert and expose has_featured_image flag X-Lovable-Edit-ID: edt-c732f5ac-dea9-43c9-ad2c-6c5ebda848f2 Co-authored-by: magnusfroste <38864257+magnusfroste@users.noreply.github.com>
2 parents a265ffe + c40d5b7 commit 8843d99

File tree

1 file changed

+78
-3
lines changed
  • supabase/functions/agent-execute

1 file changed

+78
-3
lines changed

supabase/functions/agent-execute/index.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,12 +1381,87 @@ async function executeBlogAction(
13811381
excerpt = plainText.substring(0, 160) + (plainText.length > 160 ? '...' : '');
13821382
}
13831383

1384-
const { data, error } = await supabase.from('blog_posts').insert({
1384+
// --- Auto-fetch featured image ---
1385+
let featuredImage: string | null = null;
1386+
let featuredImageAlt: string | null = null;
1387+
const imageQuery = topic || title;
1388+
1389+
// Strategy 1: Unsplash (free, fast, high quality photos)
1390+
const unsplashKey = Deno.env.get('UNSPLASH_ACCESS_KEY');
1391+
if (!featuredImage && unsplashKey) {
1392+
try {
1393+
const searchUrl = new URL('https://api.unsplash.com/search/photos');
1394+
searchUrl.searchParams.set('query', imageQuery);
1395+
searchUrl.searchParams.set('per_page', '1');
1396+
searchUrl.searchParams.set('orientation', 'landscape');
1397+
const uResp = await fetch(searchUrl.toString(), {
1398+
headers: { 'Authorization': `Client-ID ${unsplashKey}`, 'Accept-Version': 'v1' },
1399+
});
1400+
if (uResp.ok) {
1401+
const uData = await uResp.json();
1402+
const photo = uData.results?.[0];
1403+
if (photo) {
1404+
featuredImage = photo.urls?.regular;
1405+
featuredImageAlt = photo.alt_description || photo.description || `Photo by ${photo.user?.name} on Unsplash`;
1406+
console.log(`[write_blog_post] Unsplash image found: ${featuredImage}`);
1407+
}
1408+
}
1409+
} catch (e) {
1410+
console.error('[write_blog_post] Unsplash fetch failed:', e);
1411+
}
1412+
}
1413+
1414+
// Strategy 2: Gemini image generation via Lovable AI gateway
1415+
const lovableApiKey = Deno.env.get('LOVABLE_API_KEY');
1416+
if (!featuredImage && lovableApiKey) {
1417+
try {
1418+
const imgResp = await fetch('https://ai.gateway.lovable.dev/v1/chat/completions', {
1419+
method: 'POST',
1420+
headers: { 'Authorization': `Bearer ${lovableApiKey}`, 'Content-Type': 'application/json' },
1421+
body: JSON.stringify({
1422+
model: 'google/gemini-2.5-flash-image',
1423+
messages: [{ role: 'user', content: `Generate a professional, modern blog header image for an article titled "${title}" about "${topic}". The image should be visually striking, landscape oriented, suitable as a blog featured image. No text in the image.` }],
1424+
modalities: ['image', 'text'],
1425+
}),
1426+
});
1427+
if (imgResp.ok) {
1428+
const imgData = await imgResp.json();
1429+
const base64Url = imgData.choices?.[0]?.message?.images?.[0]?.image_url?.url;
1430+
if (base64Url) {
1431+
// Upload base64 image to Supabase storage
1432+
const base64Data = base64Url.replace(/^data:image\/\w+;base64,/, '');
1433+
const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
1434+
const fileName = `blog/${slug}-${Date.now()}.png`;
1435+
const { error: uploadErr } = await supabase.storage
1436+
.from('cms-images')
1437+
.upload(fileName, imageBytes, { contentType: 'image/png', upsert: true });
1438+
if (!uploadErr) {
1439+
const { data: urlData } = supabase.storage.from('cms-images').getPublicUrl(fileName);
1440+
featuredImage = urlData.publicUrl;
1441+
featuredImageAlt = `Featured image for ${title}`;
1442+
console.log(`[write_blog_post] Gemini image generated and uploaded: ${featuredImage}`);
1443+
} else {
1444+
console.error('[write_blog_post] Image upload failed:', uploadErr.message);
1445+
}
1446+
}
1447+
}
1448+
} catch (e) {
1449+
console.error('[write_blog_post] Gemini image generation failed:', e);
1450+
}
1451+
}
1452+
1453+
const insertData: Record<string, unknown> = {
13851454
title, slug, status: 'draft', excerpt, content_json: tiptapDoc,
13861455
meta_json: { tone, language, generated_by: 'flowpilot', topic },
1387-
}).select().single();
1456+
};
1457+
if (featuredImage) {
1458+
insertData.featured_image = featuredImage;
1459+
insertData.featured_image_alt = featuredImageAlt;
1460+
}
1461+
1462+
const { data, error } = await supabase.from('blog_posts').insert(insertData).select().single();
13881463
if (error) throw new Error(`Blog insert failed: ${error.message}`);
1389-
return { blog_post_id: data.id, slug: data.slug, title: data.title, status: 'draft', has_content: !!markdownContent };
1464+
return { blog_post_id: data.id, slug: data.slug, title: data.title, status: 'draft', has_content: !!markdownContent, has_featured_image: !!featuredImage };
13901465
}
13911466

13921467
// =============================================================================

0 commit comments

Comments
 (0)