fix: include linkedin_post in GET /api/blog response for SLL matching
This commit is contained in:
parent
931588fffd
commit
95a8aa8552
@ -16,8 +16,8 @@ import { pool } from "../db/client";
|
|||||||
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
const pipelineProgress = new Map<string, { step: number; total: number; label: string; pct: number }>();
|
||||||
|
|
||||||
function setProgress(draftId: string, step: number, label: string): void {
|
function setProgress(draftId: string, step: number, label: string): void {
|
||||||
const pct = Math.round((step / 14) * 92) + 2; // 2%..94% during run, 100% on complete
|
const pct = Math.round((step / 17) * 92) + 2; // 2%..94% during run, 100% on complete
|
||||||
pipelineProgress.set(draftId, { step, total: 14, label, pct });
|
pipelineProgress.set(draftId, { step, total: 17, label, pct });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearProgress(draftId: string): void {
|
function clearProgress(draftId: string): void {
|
||||||
@ -1001,20 +1001,24 @@ async function runLlmPipeline(
|
|||||||
STEP5_REALITY_INJECTION,
|
STEP5_REALITY_INJECTION,
|
||||||
STEP6_TECHNICAL_DEEPENING,
|
STEP6_TECHNICAL_DEEPENING,
|
||||||
STEP7_OPINION_LAYER,
|
STEP7_OPINION_LAYER,
|
||||||
|
STEP_AFE,
|
||||||
STEP8_KILL_AI_TONE,
|
STEP8_KILL_AI_TONE,
|
||||||
STEP8b_REDUCTION,
|
STEP8b_REDUCTION,
|
||||||
|
STEP_AEM,
|
||||||
STEP8c_STYLE_LOCK,
|
STEP8c_STYLE_LOCK,
|
||||||
STEP9_QA_CHECK,
|
STEP9_QA_CHECK,
|
||||||
STEP10_QUALITY_SCORE,
|
STEP10_QUALITY_SCORE,
|
||||||
|
STEP_APM,
|
||||||
STEP_LINKEDIN_POST,
|
STEP_LINKEDIN_POST,
|
||||||
BLOG_TYPES,
|
BLOG_TYPES,
|
||||||
buildFeedbackContext,
|
buildFeedbackContext,
|
||||||
|
buildSLLContext,
|
||||||
withCalibration,
|
withCalibration,
|
||||||
} = await import("../llm/fo-blog-pipeline");
|
} = await import("../llm/fo-blog-pipeline");
|
||||||
|
|
||||||
const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 };
|
const LLM_OPTS = { temperature: 0.7, maxTokens: 8192, timeoutMs: 480000 };
|
||||||
const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 };
|
const LLM_REFINE = { temperature: 0.4, maxTokens: 6144, timeoutMs: 480000 };
|
||||||
const TOTAL_STEPS = 14; // 10 original + 4b Narrative Control + 8b Reduction + 8c Style Lock + LinkedIn
|
const TOTAL_STEPS = 17; // 16-step pipeline + APM final cut
|
||||||
let stepsCompleted = 0;
|
let stepsCompleted = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1034,7 +1038,14 @@ async function runLlmPipeline(
|
|||||||
})));
|
})));
|
||||||
} catch { /* no feedback yet, that's fine */ }
|
} catch { /* no feedback yet, that's fine */ }
|
||||||
|
|
||||||
const systemPrompt = withCalibration(FO_BLOG_SYSTEM_PROMPT + feedbackContext);
|
// Load SLL learned patterns (safe-fails if no data yet)
|
||||||
|
let sllContext = "";
|
||||||
|
try {
|
||||||
|
sllContext = await buildSLLContext();
|
||||||
|
if (sllContext) console.log(" SLL: Learned patterns injected into system prompt");
|
||||||
|
} catch { /* no SLL data yet, fine */ }
|
||||||
|
|
||||||
|
const systemPrompt = withCalibration(FO_BLOG_SYSTEM_PROMPT + feedbackContext + sllContext);
|
||||||
|
|
||||||
// Warmup
|
// Warmup
|
||||||
await generate("Test", "OK", { temperature: 0.1, maxTokens: 8, timeoutMs: 60000 }).catch(() => {});
|
await generate("Test", "OK", { temperature: 0.1, maxTokens: 8, timeoutMs: 60000 }).catch(() => {});
|
||||||
@ -1147,66 +1158,88 @@ async function runLlmPipeline(
|
|||||||
stepsCompleted = 6;
|
stepsCompleted = 6;
|
||||||
|
|
||||||
// ═══ STEP 6: Technical Deepening ═══
|
// ═══ STEP 6: Technical Deepening ═══
|
||||||
console.log(" Step 7/13: Technical Deepening...");
|
console.log(" Step 7/16: Technical Deepening...");
|
||||||
setProgress(draftId, 7, "Step 7/13: Technical Deepening");
|
setProgress(draftId, 7, "Step 7/16: Technical Deepening");
|
||||||
const step6 = await generate(systemPrompt,
|
const step6 = await generate(systemPrompt,
|
||||||
STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text),
|
STEP6_TECHNICAL_DEEPENING.replace("{{ARTICLE}}", step5.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 6;
|
stepsCompleted = 7;
|
||||||
|
|
||||||
// ═══ STEP 7: Opinion Layer ═══
|
// ═══ STEP 7: Opinion Layer ═══
|
||||||
console.log(" Step 8/13: Opinion Layer...");
|
console.log(" Step 8/16: Opinion Layer...");
|
||||||
setProgress(draftId, 8, "Step 8/13: Opinion Layer");
|
setProgress(draftId, 8, "Step 8/16: Opinion Layer");
|
||||||
const step7 = await generate(systemPrompt,
|
const step7 = await generate(systemPrompt,
|
||||||
STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text),
|
STEP7_OPINION_LAYER.replace("{{ARTICLE}}", step6.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 8;
|
stepsCompleted = 8;
|
||||||
|
|
||||||
// ═══ STEP 8: Kill AI Tone ═══
|
// ═══ STEP AFE: Auto-Focus Enforcer (ONE idea, ONE scenario, kill drift) ═══
|
||||||
console.log(" Step 9/13: Kill AI Tone...");
|
console.log(" Step 9/16: Auto-Focus Enforcer (kill multi-topic drift)...");
|
||||||
setProgress(draftId, 9, "Step 9/13: Kill AI Tone");
|
setProgress(draftId, 9, "Step 9/16: Auto-Focus Enforcer");
|
||||||
const step8 = await generate(systemPrompt,
|
const stepAFE = await generate(systemPrompt,
|
||||||
STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", step7.text),
|
STEP_AFE.replace("{{ARTICLE}}", step7.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 9;
|
stepsCompleted = 9;
|
||||||
|
const wordsAFE = stepAFE.text.split(/\s+/).length;
|
||||||
|
const wordsBeforeAFE = step7.text.split(/\s+/).length;
|
||||||
|
const pctAFE = Math.round((1 - wordsAFE / wordsBeforeAFE) * 100);
|
||||||
|
if (pctAFE > 5) console.log(` AFE cut: ${wordsBeforeAFE} → ${wordsAFE} words (−${pctAFE}%) — drift removed`);
|
||||||
|
|
||||||
|
// ═══ STEP 8: Kill AI Tone ═══
|
||||||
|
console.log(" Step 10/16: Kill AI Tone...");
|
||||||
|
setProgress(draftId, 10, "Step 10/16: Kill AI Tone");
|
||||||
|
const step8 = await generate(systemPrompt,
|
||||||
|
STEP8_KILL_AI_TONE.replace("{{ARTICLE}}", stepAFE.text),
|
||||||
|
LLM_REFINE
|
||||||
|
);
|
||||||
|
stepsCompleted = 10;
|
||||||
|
|
||||||
// ═══ STEP 8b: Reduction Engine (5-pass: Repetition Kill → Tech Prune → Flow Rebuild → Weight Correction → Humanization) ═══
|
// ═══ STEP 8b: Reduction Engine (5-pass: Repetition Kill → Tech Prune → Flow Rebuild → Weight Correction → Humanization) ═══
|
||||||
console.log(" Step 10/14: Reduction Engine (5-pass, target 700-1000 words)...");
|
console.log(" Step 11/16: Reduction Engine (5-pass, target 700-1000 words)...");
|
||||||
setProgress(draftId, 10, "Step 10/14: Reduction Engine");
|
setProgress(draftId, 11, "Step 11/16: Reduction Engine");
|
||||||
const step8b = await generate(systemPrompt,
|
const step8b = await generate(systemPrompt,
|
||||||
STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text),
|
STEP8b_REDUCTION.replace("{{ARTICLE}}", step8.text),
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 10;
|
stepsCompleted = 11;
|
||||||
const wordsAfter = step8b.text.split(/\s+/).length;
|
const wordsAfter = step8b.text.split(/\s+/).length;
|
||||||
const wordsBefore = step8.text.split(/\s+/).length;
|
const wordsBefore = step8.text.split(/\s+/).length;
|
||||||
const pctChange = Math.round((1 - wordsAfter / wordsBefore) * 100);
|
const pctChange = Math.round((1 - wordsAfter / wordsBefore) * 100);
|
||||||
console.log(` After reduction: ${wordsAfter} words (was ${wordsBefore}, −${pctChange}%) ${wordsAfter > 1300 ? "⚠ WARNING: >1300 words" : wordsAfter < 600 ? "⚠ WARNING: <600 words" : "✓ in target range"}`);
|
console.log(` After reduction: ${wordsAfter} words (was ${wordsBefore}, −${pctChange}%) ${wordsAfter > 1300 ? "⚠ WARNING: >1300 words" : wordsAfter < 600 ? "⚠ WARNING: <600 words" : "✓ in target range"}`);
|
||||||
|
|
||||||
// ═══ STEP 8c: Style Lock ═══
|
// ═══ STEP AEM: Auto-Editor Mode (Senior Engineer voice polish) ═══
|
||||||
console.log(" Step 11/14: Style Lock (tone consistency + scope/SKU fixes)...");
|
console.log(" Step 12/16: Auto-Editor Mode (senior engineer voice polish)...");
|
||||||
setProgress(draftId, 11, "Step 11/14: Style Lock");
|
setProgress(draftId, 12, "Step 12/16: Auto-Editor Mode");
|
||||||
const step8c = await generate(systemPrompt,
|
const stepAEM = await generate(systemPrompt,
|
||||||
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", step8b.text),
|
STEP_AEM.replace("{{ARTICLE}}", step8b.text),
|
||||||
LLM_REFINE
|
|
||||||
);
|
|
||||||
stepsCompleted = 11;
|
|
||||||
|
|
||||||
// ═══ STEP 9: QA Check ═══
|
|
||||||
console.log(" Step 12/14: QA Check...");
|
|
||||||
setProgress(draftId, 12, "Step 12/14: QA Check");
|
|
||||||
const step9 = await generate(systemPrompt,
|
|
||||||
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
|
||||||
LLM_REFINE
|
LLM_REFINE
|
||||||
);
|
);
|
||||||
stepsCompleted = 12;
|
stepsCompleted = 12;
|
||||||
|
|
||||||
|
// ═══ STEP 8c: Style Lock ═══
|
||||||
|
console.log(" Step 13/16: Style Lock (tone consistency + scope/SKU fixes)...");
|
||||||
|
setProgress(draftId, 13, "Step 13/16: Style Lock");
|
||||||
|
const step8c = await generate(systemPrompt,
|
||||||
|
STEP8c_STYLE_LOCK.replace("{{ARTICLE}}", stepAEM.text),
|
||||||
|
LLM_REFINE
|
||||||
|
);
|
||||||
|
stepsCompleted = 13;
|
||||||
|
|
||||||
|
// ═══ STEP 9: QA Check ═══
|
||||||
|
console.log(" Step 14/16: QA Check...");
|
||||||
|
setProgress(draftId, 14, "Step 14/16: QA Check");
|
||||||
|
const step9 = await generate(systemPrompt,
|
||||||
|
STEP9_QA_CHECK.replace("{{ARTICLE}}", step8c.text),
|
||||||
|
LLM_REFINE
|
||||||
|
);
|
||||||
|
stepsCompleted = 14;
|
||||||
|
|
||||||
// ═══ STEP 10: Quality Score ═══
|
// ═══ STEP 10: Quality Score ═══
|
||||||
console.log(" Step 13/14: Quality Score...");
|
console.log(" Step 15/16: Quality Score...");
|
||||||
setProgress(draftId, 13, "Step 13/14: Quality Score");
|
setProgress(draftId, 15, "Step 15/16: Quality Score");
|
||||||
let autoQaScore: Record<string, unknown> | null = null;
|
let autoQaScore: Record<string, unknown> | null = null;
|
||||||
try {
|
try {
|
||||||
const step10 = await generate(systemPrompt,
|
const step10 = await generate(systemPrompt,
|
||||||
@ -1222,16 +1255,29 @@ async function runLlmPipeline(
|
|||||||
} catch {
|
} catch {
|
||||||
console.log(" Quality scoring skipped (parse error)");
|
console.log(" Quality scoring skipped (parse error)");
|
||||||
}
|
}
|
||||||
stepsCompleted = 13;
|
stepsCompleted = 15;
|
||||||
|
|
||||||
|
// ═══ STEP APM: Auto-Precision Mode (Final Cut — last filter before publish) ═══
|
||||||
|
console.log(" Step 16/17: Auto-Precision Mode (final cut — if a word can go, it must go)...");
|
||||||
|
setProgress(draftId, 16, "Step 16/17: Auto-Precision Mode");
|
||||||
|
const stepAPM = await generate(systemPrompt,
|
||||||
|
STEP_APM.replace("{{ARTICLE}}", step9.text),
|
||||||
|
LLM_REFINE
|
||||||
|
);
|
||||||
|
stepsCompleted = 16;
|
||||||
|
const wordsAPM = stepAPM.text.split(/\s+/).length;
|
||||||
|
const wordsBeforeAPM = step9.text.split(/\s+/).length;
|
||||||
|
const pctAPM = Math.round((1 - wordsAPM / wordsBeforeAPM) * 100);
|
||||||
|
console.log(` APM: ${wordsBeforeAPM} → ${wordsAPM} words (−${pctAPM}%) — precision cut done`);
|
||||||
|
|
||||||
// ═══ LinkedIn Post ═══
|
// ═══ LinkedIn Post ═══
|
||||||
console.log(" Step 14/14: LinkedIn Post (max 2,800 chars)...");
|
console.log(" Step 17/17: LinkedIn Post (max 2,800 chars)...");
|
||||||
setProgress(draftId, 14, "Step 14/14: LinkedIn Post");
|
setProgress(draftId, 17, "Step 17/17: LinkedIn Post");
|
||||||
let linkedinPost: string | null = null;
|
let linkedinPost: string | null = null;
|
||||||
let linkedinCharCount: number | null = null;
|
let linkedinCharCount: number | null = null;
|
||||||
try {
|
try {
|
||||||
const stepLinkedIn = await generate(systemPrompt,
|
const stepLinkedIn = await generate(systemPrompt,
|
||||||
STEP_LINKEDIN_POST.replace("{{ARTICLE}}", step9.text),
|
STEP_LINKEDIN_POST.replace("{{ARTICLE}}", stepAPM.text),
|
||||||
{ temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 }
|
{ temperature: 0.6, maxTokens: 1024, timeoutMs: 120000 }
|
||||||
);
|
);
|
||||||
linkedinPost = stepLinkedIn.text.trim();
|
linkedinPost = stepLinkedIn.text.trim();
|
||||||
@ -1247,11 +1293,11 @@ async function runLlmPipeline(
|
|||||||
} catch {
|
} catch {
|
||||||
console.log(" LinkedIn post generation skipped");
|
console.log(" LinkedIn post generation skipped");
|
||||||
}
|
}
|
||||||
stepsCompleted = 14;
|
stepsCompleted = 17;
|
||||||
|
|
||||||
// Extract only the article from STEP9 output (QA returns review + fixed article)
|
// Extract only the article from APM output (APM returns clean article only)
|
||||||
// Look for "COMPLETE FIXED ARTICLE" marker and take everything after it
|
// Fall back to step9.text if APM output looks too short or empty
|
||||||
let finalArticleText = step9.text;
|
let finalArticleText = stepAPM.text.trim().length > 200 ? stepAPM.text : step9.text;
|
||||||
const articleMarkers = [
|
const articleMarkers = [
|
||||||
"### COMPLETE FIXED ARTICLE",
|
"### COMPLETE FIXED ARTICLE",
|
||||||
"## COMPLETE FIXED ARTICLE",
|
"## COMPLETE FIXED ARTICLE",
|
||||||
@ -1259,13 +1305,16 @@ async function runLlmPipeline(
|
|||||||
"---\n\n**You're",
|
"---\n\n**You're",
|
||||||
"---\n\nYou're",
|
"---\n\nYou're",
|
||||||
];
|
];
|
||||||
|
// Also check step9 for QA markers (APM may have stripped them already)
|
||||||
for (const marker of articleMarkers) {
|
for (const marker of articleMarkers) {
|
||||||
const idx = step9.text.indexOf(marker);
|
const idx = step9.text.indexOf(marker);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
// Skip past the marker line itself
|
|
||||||
const afterMarker = step9.text.slice(idx + marker.length).trimStart();
|
const afterMarker = step9.text.slice(idx + marker.length).trimStart();
|
||||||
// Strip leading --- separator if present
|
const extractedFromQA = afterMarker.replace(/^---\s*\n/, "").trimStart();
|
||||||
finalArticleText = afterMarker.replace(/^---\s*\n/, "").trimStart();
|
// Only use QA extraction if it's meaningfully longer than APM output
|
||||||
|
if (extractedFromQA.split(/\s+/).length > finalArticleText.split(/\s+/).length * 0.8) {
|
||||||
|
finalArticleText = extractedFromQA;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1280,18 +1329,29 @@ async function runLlmPipeline(
|
|||||||
const wordCount = draftContent.split(/\s+/).length;
|
const wordCount = draftContent.split(/\s+/).length;
|
||||||
const finalIssues = validateArticle(draftContent);
|
const finalIssues = validateArticle(draftContent);
|
||||||
|
|
||||||
// Update the draft in DB
|
// Hard minimum word count gate (1200 for LLM pipeline)
|
||||||
|
if (wordCount < 1200) {
|
||||||
|
const shortMsg = `⚠ WORD COUNT FAIL: ${wordCount} words — minimum 1200 for LLM pipeline`;
|
||||||
|
console.log(` ${shortMsg}`);
|
||||||
|
if (!finalIssues.includes(`Too short: ${wordCount} words`)) {
|
||||||
|
finalIssues.push(`Too short: ${wordCount} words (minimum 1200 for LLM pipeline — article needs expansion)`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ✓ Word count: ${wordCount} words (≥1200 — OK)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the draft in DB — promote to 'ready' on full pipeline completion
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE blog_drafts
|
`UPDATE blog_drafts
|
||||||
SET draft_content = $1, word_count = $2,
|
SET draft_content = $1, word_count = $2,
|
||||||
generated_by = 'fo-blog-engine-v5',
|
generated_by = 'fo-blog-engine-v6',
|
||||||
pipeline_version = 'v5-narrative-control',
|
pipeline_version = 'v6-precision-mode',
|
||||||
pipeline_steps_completed = $3,
|
pipeline_steps_completed = $3,
|
||||||
auto_qa_score = $4,
|
auto_qa_score = $4,
|
||||||
outline = $5,
|
outline = $5,
|
||||||
linkedin_post = $6,
|
linkedin_post = $6,
|
||||||
linkedin_char_count = $7,
|
linkedin_char_count = $7,
|
||||||
status = 'draft',
|
status = 'review',
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $8::uuid`,
|
WHERE id = $8::uuid`,
|
||||||
[
|
[
|
||||||
@ -1329,10 +1389,10 @@ async function runLlmPipeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearProgress(draftId);
|
clearProgress(draftId);
|
||||||
console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/14 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10, LinkedIn: ${linkedinCharCount ?? "n/a"} chars`);
|
console.log(`Blog FO Pipeline: ${draftId} complete — ${wordCount} words, ${stepsCompleted}/17 steps, QA: ${(autoQaScore as any)?.overall || "N/A"}/10, LinkedIn: ${linkedinCharCount ?? "n/a"} chars`);
|
||||||
} catch (llmErr) {
|
} catch (llmErr) {
|
||||||
clearProgress(draftId);
|
clearProgress(draftId);
|
||||||
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/14 for ${draftId}: ${(llmErr as Error).message}`);
|
console.warn(`Blog FO Pipeline failed at step ${stepsCompleted + 1}/16 for ${draftId}: ${(llmErr as Error).message}`);
|
||||||
// Update with partial progress
|
// Update with partial progress
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v5-narrative-control',
|
`UPDATE blog_drafts SET pipeline_steps_completed = $1, pipeline_version = 'v5-narrative-control',
|
||||||
@ -1382,6 +1442,19 @@ blogRouter.post("/generate", async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const data = await gatherBlogData(keywords, selectedTopic);
|
const data = await gatherBlogData(keywords, selectedTopic);
|
||||||
|
|
||||||
|
// Clean up stale template drafts for the same title (idempotent regeneration)
|
||||||
|
// If a template draft already exists for this title, remove it before creating a fresh one
|
||||||
|
await pool.query(
|
||||||
|
`DELETE FROM blog_feedback WHERE blog_id IN (
|
||||||
|
SELECT id FROM blog_drafts WHERE title = $1 AND generated_by = 'tip-blog-engine-template'
|
||||||
|
)`,
|
||||||
|
[title]
|
||||||
|
).catch(() => {});
|
||||||
|
await pool.query(
|
||||||
|
`DELETE FROM blog_drafts WHERE title = $1 AND generated_by = 'tip-blog-engine-template'`,
|
||||||
|
[title]
|
||||||
|
).catch(() => {});
|
||||||
|
|
||||||
// Always create a template draft first (instant response)
|
// Always create a template draft first (instant response)
|
||||||
const draftContent = generateTemplateDraft(title, selectedTopic, data);
|
const draftContent = generateTemplateDraft(title, selectedTopic, data);
|
||||||
const wordCount = draftContent.split(/\s+/).length;
|
const wordCount = draftContent.split(/\s+/).length;
|
||||||
@ -1459,7 +1532,7 @@ blogRouter.post("/generate", async (req: Request, res: Response) => {
|
|||||||
blogRouter.get("/", async (_req: Request, res: Response) => {
|
blogRouter.get("/", async (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`SELECT id, title, topic, target_audience, status, word_count, seo_keywords, generated_by, created_at
|
`SELECT id, title, topic, target_audience, status, word_count, seo_keywords, generated_by, created_at, linkedin_post
|
||||||
FROM blog_drafts
|
FROM blog_drafts
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 50`,
|
LIMIT 50`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user