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
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:
@@ -22,6 +22,7 @@ export interface OptimizationTask {
|
|||||||
export interface OptimizeFileOptions {
|
export interface OptimizeFileOptions {
|
||||||
contextDir: string;
|
contextDir: string;
|
||||||
availableComponents?: ComponentDefinition[];
|
availableComponents?: ComponentDefinition[];
|
||||||
|
shouldRename?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AiBlogPostOrchestrator {
|
export class AiBlogPostOrchestrator {
|
||||||
@@ -115,7 +116,7 @@ export class AiBlogPostOrchestrator {
|
|||||||
let finalPath = absPath;
|
let finalPath = absPath;
|
||||||
let finalSlug = path.basename(absPath, ".mdx");
|
let finalSlug = path.basename(absPath, ".mdx");
|
||||||
|
|
||||||
if (newFmMatch && newFmMatch[1]) {
|
if (options.shouldRename && newFmMatch && newFmMatch[1]) {
|
||||||
const titleMatch = newFmMatch[1].match(/title:\s*["']([^"']+)["']/);
|
const titleMatch = newFmMatch[1].match(/title:\s*["']([^"']+)["']/);
|
||||||
if (titleMatch && titleMatch[1]) {
|
if (titleMatch && titleMatch[1]) {
|
||||||
const newTitle = titleMatch[1];
|
const newTitle = titleMatch[1];
|
||||||
@@ -137,12 +138,14 @@ export class AiBlogPostOrchestrator {
|
|||||||
// Delete old file if the title changed significantly
|
// Delete old file if the title changed significantly
|
||||||
try {
|
try {
|
||||||
await fs.unlink(absPath);
|
await fs.unlink(absPath);
|
||||||
} catch (e) {
|
} catch (_err) {
|
||||||
/* ignore */
|
// ignore
|
||||||
}
|
}
|
||||||
finalPath = newAbsPath;
|
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
|
// Idea 5: Automatic Thumbnails
|
||||||
@@ -154,10 +157,8 @@ export class AiBlogPostOrchestrator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.thumbnailGenerator && !hasExistingThumbnail) {
|
if (this.thumbnailGenerator && !hasExistingThumbnail) {
|
||||||
console.log("🎨 Phase 5: Generating visual thumbnail...");
|
console.log("🎨 Phase 5: Generating/Linking visual thumbnail...");
|
||||||
try {
|
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 webPublicDir = path.resolve(process.cwd(), "apps/web/public");
|
||||||
const thumbnailRelPath = `/blog/${finalSlug}.png`;
|
const thumbnailRelPath = `/blog/${finalSlug}.png`;
|
||||||
const thumbnailAbsPath = path.join(
|
const thumbnailAbsPath = path.join(
|
||||||
@@ -166,12 +167,26 @@ export class AiBlogPostOrchestrator {
|
|||||||
`${finalSlug}.png`,
|
`${finalSlug}.png`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.thumbnailGenerator.generateImage(
|
// Check if the physical file already exists
|
||||||
visualPrompt,
|
let physicalFileExists = false;
|
||||||
thumbnailAbsPath,
|
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:")) {
|
if (finalContent.includes("thumbnail:")) {
|
||||||
finalContent = finalContent.replace(
|
finalContent = finalContent.replace(
|
||||||
/thumbnail:\s*["'].*?["']/,
|
/thumbnail:\s*["'].*?["']/,
|
||||||
@@ -184,7 +199,7 @@ export class AiBlogPostOrchestrator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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):
|
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.
|
- 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.
|
- 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.
|
- 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).
|
||||||
- Zitat-Varianten: Wenn du Organisationen oder Studien zitierst, nutze \`<ArticleQuote isCompany={true} ... />\`. Für Personen lass \`isCompany\` weg.
|
- 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 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.
|
- Verwende unsere Komponenten stilvoll für Visualisierungen.
|
||||||
- Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body.
|
- Agiere als hochprofessioneller Digital Architect und entferne alte MDX-Metadaten im Body.
|
||||||
- Fazit: Schließe JEDEN Artikel ZWINGEND mit einem starken, klaren 'Fazit' ab.
|
- 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:
|
STRICT MDX OUTPUT RULES:
|
||||||
1. ONLY use the exact components defined above.
|
1. ONLY use the exact components defined above.
|
||||||
|
|||||||
@@ -190,11 +190,21 @@ Return JSON: { "facts": [ { "statement": "...", "source": "Organization Name Onl
|
|||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: `Generate a YouTube search query to find a high-quality, professional educational video about: "${topic}".
|
content: `You generate ultra-short, highly relevant YouTube search queries based on a given text context.
|
||||||
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"}.
|
RULES:
|
||||||
DO NOT USE QUOTES IN THE QUERY ITSELF.`,
|
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" },
|
response_format: { type: "json_object" },
|
||||||
});
|
});
|
||||||
@@ -289,7 +299,7 @@ CRITICAL: Do NOT provide more than 2 trendsKeywords. Keep it extremely focused.`
|
|||||||
try {
|
try {
|
||||||
let parsed = JSON.parse(
|
let parsed = JSON.parse(
|
||||||
response.choices[0].message.content ||
|
response.choices[0].message.content ||
|
||||||
'{"trendsKeywords": [], "dcVariables": []}',
|
'{"trendsKeywords": [], "dcVariables": []}',
|
||||||
);
|
);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };
|
parsed = parsed[0] || { trendsKeywords: [], dcVariables: [] };
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ export interface ThumbnailGeneratorConfig {
|
|||||||
replicateApiKey: string;
|
replicateApiKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThumbnailGenerateOptions {
|
||||||
|
model?: string;
|
||||||
|
systemPrompt?: string;
|
||||||
|
imagePrompt?: string; // Path to local reference image
|
||||||
|
}
|
||||||
|
|
||||||
export class ThumbnailGenerator {
|
export class ThumbnailGenerator {
|
||||||
private replicate: Replicate;
|
private replicate: Replicate;
|
||||||
|
|
||||||
@@ -18,21 +24,43 @@ export class ThumbnailGenerator {
|
|||||||
public async generateImage(
|
public async generateImage(
|
||||||
topic: string,
|
topic: string,
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
|
options?: ThumbnailGenerateOptions,
|
||||||
): Promise<string> {
|
): 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}`;
|
const prompt = `${systemPrompt}\n\nTopic to illustrate abstractly: ${topic}`;
|
||||||
|
|
||||||
console.log(`🎨 Generating thumbnail for topic: "${topic}"...`);
|
console.log(`🎨 Generating thumbnail for topic: "${topic}"...`);
|
||||||
|
|
||||||
const output = await this.replicate.run("black-forest-labs/flux-1.1-pro", {
|
let inputPayload: any = {
|
||||||
input: {
|
prompt,
|
||||||
prompt,
|
aspect_ratio: "16:9",
|
||||||
aspect_ratio: "16:9",
|
output_format: "png",
|
||||||
output_format: "png",
|
output_quality: 90,
|
||||||
output_quality: 90,
|
prompt_upsampling: false,
|
||||||
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
|
// Replicate returns a ReadableStream for the output of flux-1.1-pro in newer Node SDKs
|
||||||
|
|||||||
Reference in New Issue
Block a user