From f4507ef1214fa33f40fde47138919e2674b3c615 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 22 Feb 2026 17:35:38 +0100 Subject: [PATCH] fix(journaling): optimize serper video search queries to prevent MDX hallucination --- packages/content-engine/src/orchestrator.ts | 49 +++++++++++++------ packages/journaling/src/agent.ts | 20 ++++++-- packages/thumbnail-generator/src/generator.ts | 46 +++++++++++++---- 3 files changed, 85 insertions(+), 30 deletions(-) diff --git a/packages/content-engine/src/orchestrator.ts b/packages/content-engine/src/orchestrator.ts index 59598df..8c1f453 100644 --- a/packages/content-engine/src/orchestrator.ts +++ b/packages/content-engine/src/orchestrator.ts @@ -22,6 +22,7 @@ export interface OptimizationTask { export interface OptimizeFileOptions { contextDir: string; availableComponents?: ComponentDefinition[]; + shouldRename?: boolean; } export class AiBlogPostOrchestrator { @@ -115,7 +116,7 @@ export class AiBlogPostOrchestrator { let finalPath = absPath; let finalSlug = path.basename(absPath, ".mdx"); - if (newFmMatch && newFmMatch[1]) { + if (options.shouldRename && newFmMatch && newFmMatch[1]) { const titleMatch = newFmMatch[1].match(/title:\s*["']([^"']+)["']/); if (titleMatch && titleMatch[1]) { const newTitle = titleMatch[1]; @@ -137,12 +138,14 @@ export class AiBlogPostOrchestrator { // Delete old file if the title changed significantly try { await fs.unlink(absPath); - } catch (e) { - /* ignore */ + } catch (_err) { + // ignore } finalPath = newAbsPath; } } + } else if (newFmMatch && newFmMatch[1]) { + console.log(`ℹ️ Rename skipped (permalink stability active). If you want to rename, use --rename.`); } // Idea 5: Automatic Thumbnails @@ -154,10 +157,8 @@ export class AiBlogPostOrchestrator { ); if (this.thumbnailGenerator && !hasExistingThumbnail) { - console.log("🎨 Phase 5: Generating visual thumbnail..."); + console.log("🎨 Phase 5: Generating/Linking visual thumbnail..."); try { - const visualPrompt = await this.generateVisualPrompt(finalContent); - // We assume public dir is relative to where this runs, usually monorepo root or apps/web const webPublicDir = path.resolve(process.cwd(), "apps/web/public"); const thumbnailRelPath = `/blog/${finalSlug}.png`; const thumbnailAbsPath = path.join( @@ -166,12 +167,26 @@ export class AiBlogPostOrchestrator { `${finalSlug}.png`, ); - await this.thumbnailGenerator.generateImage( - visualPrompt, - thumbnailAbsPath, - ); + // Check if the physical file already exists + let physicalFileExists = false; + try { + await fs.access(thumbnailAbsPath); + physicalFileExists = true; + } catch (_err) { + // File does not exist + } - // Update frontmatter with thumbnail (SEO: we also want it as a hero) + if (physicalFileExists) { + console.log(`⏭️ Thumbnail already exists on disk, skipping generation: ${thumbnailAbsPath}`); + } else { + const visualPrompt = await this.generateVisualPrompt(finalContent); + await this.thumbnailGenerator.generateImage( + visualPrompt, + thumbnailAbsPath, + ); + } + + // Update frontmatter with thumbnail if (finalContent.includes("thumbnail:")) { finalContent = finalContent.replace( /thumbnail:\s*["'].*?["']/, @@ -184,7 +199,7 @@ export class AiBlogPostOrchestrator { ); } } catch (e) { - console.warn("⚠️ Thumbnail generation failed, skipping:", e); + console.warn("⚠️ Thumbnail processing failed, skipping:", e); } } @@ -421,14 +436,16 @@ ${task.instructions || "None"} BLOG POST BEST PRACTICES (MANDATORY): - DEVIL'S ADVOCATE: Füge zwingend eine kurze kritische Sektion ein (z.B. mit \`\` oder \`\`), in der du offen die Nachteile/Kosten/Haken deiner eigenen Lösung ansprichst ("Der Haken an der Sache..."). Das baut Vertrauen bei B2B Entscheidenden auf. -- FAQ GENERATOR: Am absoluten Ende des Artikels erstellst du zwingend eine Markdown-Liste mit den 3 wichtigsten Fragen (FAQ) und Antworten (jeweils 2 Sätze) für Google Rich Snippets. Nutze dazu das \`\` Component oder normales Markdown. -- SUBTLE CTAs: Webe 1-2 subtile CTAs für High-End Website Entwicklung ein (Beispiel: \`\` oder \`\`). Platziere diese zwingend organisch nach Abschnitten mit hohem Mehrwert. -- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze \`\`. Für Personen lass \`isCompany\` weg. +- FAQ GENERATOR: Am absoluten Ende des Artikels erstellst du zwingend eine Markdown-Liste mit den 3 wichtigsten Fragen (FAQ) und Antworten (jeweils 2 Sätze) für Google Rich Snippets. Nutze dazu das FAQSection Component oder normales Markdown. +- SUBTLE CTAs: Webe 1-2 subtile CTAs für High-End Website Entwicklung ein. Nutze ZWINGEND die Komponente [LeadMagnet] für diese Zwecke anstelle von einfachen Buttons. [LeadMagnet] bietet mehr Kontext und Vertrauen. Beispiel: . Die Texte im LeadMagnet müssen absolut überzeugend, hochprofessionell und B2B-fokussiert sein (KEIN Robotik-Marketing-Sprech). +- MEME DIVERSITY: Nutze abwechslungsreiche Templates für Memes (distracted, gb, fine, ds, gru, cmm, ahb, uno, disastergirl, pigeon, rollsafe, pikachu, slap, exit, mordor, panik-kalm-panik). Wiederhole NIEMALS das gleiche Template in verschiedenen Artikeln, wenn du kannst. Wähle basierend auf dem Kontext des Artikels das passendste und sarkastischste Template. +- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze ArticleQuote (mit isCompany=true für Firmen). Für Personen lass isCompany weg. - Füge zwingend ein prägnantes 'TL;DR' ganz am Anfang ein. -- Füge ein sauberes '' ein. +- Füge ein sauberes TableOfContents ein. - Verwende unsere Komponenten stilvoll für Visualisierungen. - Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body. - Fazit: Schließe JEDEN Artikel ZWINGEND mit einem starken, klaren 'Fazit' ab. +- ORIGINAL LANGUAGE QUOTES: Übersetze NIEMALS Zitate (z.B. in ArticleQuote). Behalte das Original (z.B. Englisch), wenn du Studien von Deloitte, McKinsey oder Aussagen von CEOs zitierst. Das erhöht die Authentizität im B2B-Mittelstand. STRICT MDX OUTPUT RULES: 1. ONLY use the exact components defined above. diff --git a/packages/journaling/src/agent.ts b/packages/journaling/src/agent.ts index d0cad26..074727d 100644 --- a/packages/journaling/src/agent.ts +++ b/packages/journaling/src/agent.ts @@ -190,11 +190,21 @@ Return JSON: { "facts": [ { "statement": "...", "source": "Organization Name Onl messages: [ { role: "system", - content: `Generate a YouTube search query to find a high-quality, professional educational video about: "${topic}". -Prefer official tech channels or well-known developers (e.g., Google Chrome Developers, Vercel, Theo - t3.gg, Fireship, etc.). -Return a JSON object with a single string field "query". Example: {"query": "core web vitals explanation google developers"}. -DO NOT USE QUOTES IN THE QUERY ITSELF.`, + content: `You generate ultra-short, highly relevant YouTube search queries based on a given text context. + +RULES: +1. Extract only the 2-4 most important technical or business keywords from the provided text. +2. Ignore all markdown syntax, frontmatter (---), titles, and descriptions. +3. Keep the query generic enough to find popular educational tech videos. +4. DO NOT append specific channel names (e.g., "Fireship", "Vercel") to the query. +5. DO NOT USE QUOTES IN THE QUERY. + +Return a JSON object with a single string field "query". Example: {"query": "core web vitals performance"}`, }, + { + role: "user", + content: `CONTEXT: ${topic}`, + } ], response_format: { type: "json_object" }, }); @@ -289,7 +299,7 @@ CRITICAL: Do NOT provide more than 2 trendsKeywords. Keep it extremely focused.` try { let parsed = JSON.parse( response.choices[0].message.content || - '{"trendsKeywords": [], "dcVariables": []}', + '{"trendsKeywords": [], "dcVariables": []}', ); if (Array.isArray(parsed)) { parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] }; diff --git a/packages/thumbnail-generator/src/generator.ts b/packages/thumbnail-generator/src/generator.ts index 981e9b9..add51e7 100644 --- a/packages/thumbnail-generator/src/generator.ts +++ b/packages/thumbnail-generator/src/generator.ts @@ -6,6 +6,12 @@ export interface ThumbnailGeneratorConfig { replicateApiKey: string; } +export interface ThumbnailGenerateOptions { + model?: string; + systemPrompt?: string; + imagePrompt?: string; // Path to local reference image +} + export class ThumbnailGenerator { private replicate: Replicate; @@ -18,21 +24,43 @@ export class ThumbnailGenerator { public async generateImage( topic: string, outputPath: string, + options?: ThumbnailGenerateOptions, ): Promise { - const systemPrompt = `Technical blueprint / architectural illustration — clean lines, monochrome base with one highlighter accent color (yellow, pink, or green). Abstract, geometric, or diagrammatic illustrations only. 'Engineering notebook sketch' — precise, minimal, professional. No text in images. No people or realistic photos.`; + const defaultSystemPrompt = `A highly polished, ultra-minimalist conceptual illustration. Style: high-end tech agency, clean modern 3D or flat vector art, extensive use of negative space, elegant monochrome palette (whites, light grays) with a single vibrant accent color (neon green or electric blue). Extremely clean and precise geometry. Absolutely no text, no photorealism, no chaotic lines, no messy sketches, no people.`; + const systemPrompt = options?.systemPrompt || defaultSystemPrompt; const prompt = `${systemPrompt}\n\nTopic to illustrate abstractly: ${topic}`; console.log(`🎨 Generating thumbnail for topic: "${topic}"...`); - const output = await this.replicate.run("black-forest-labs/flux-1.1-pro", { - input: { - prompt, - aspect_ratio: "16:9", - output_format: "png", - output_quality: 90, - prompt_upsampling: false, - }, + let inputPayload: any = { + prompt, + aspect_ratio: "16:9", + output_format: "png", + output_quality: 90, + prompt_upsampling: false, + }; + + if (options?.imagePrompt) { + console.log(`🖼️ Using image style reference: ${options.imagePrompt}`); + try { + const absImgPath = path.isAbsolute(options.imagePrompt) + ? options.imagePrompt + : path.resolve(process.cwd(), options.imagePrompt); + const imgBuffer = await fs.readFile(absImgPath); + const base64 = imgBuffer.toString("base64"); + // Replicate models usually expect a data URI for image_prompt + inputPayload.image_prompt = `data:image/png;base64,${base64}`; + } catch (err) { + console.warn(`⚠️ Could not load image prompt: ${err}`); + } + } + + // Default to the requested nano-banana-pro model unless explicitly provided + const model = options?.model || "google/nano-banana-pro"; + + const output = await this.replicate.run(model as `${string}/${string}`, { + input: inputPayload, }); // Replicate returns a ReadableStream for the output of flux-1.1-pro in newer Node SDKs