fix(journaling): optimize serper video search queries to prevent MDX hallucination
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧪 Test (push) Successful in 59s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m0s
Monorepo Pipeline / 🏗️ Build (push) Successful in 5m9s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped

This commit is contained in:
2026-02-22 17:35:38 +01:00
parent 3a1a88db89
commit f4507ef121
3 changed files with 85 additions and 30 deletions

View File

@@ -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 \`<ComparisonRow>\` oder \`<IconList>\`), 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 \`<FAQSection>\` Component oder normales Markdown.
- SUBTLE CTAs: Webe 1-2 subtile CTAs für High-End Website Entwicklung ein (Beispiel: \`<Button href="/contact" variant="outline" size="normal">Performance-Check anfragen</Button>\` oder \`<Button href="/contact">Digitale Architektur anfragen</Button>\`). Platziere diese zwingend organisch nach Abschnitten mit hohem Mehrwert.
- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze \`<ArticleQuote isCompany={true} ... />\`. 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: <LeadMagnet title="Performance-Check anfragen" description="Wir analysieren Ihre Core Web Vitals und decken Umsatzpotenziale auf." buttonText="Jetzt analysieren lassen" href="/contact" variant="performance" />. 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 '<TableOfContents />' 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.

View File

@@ -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: [] };

View File

@@ -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<string> {
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