Phase 1: Platform-Specific Prompt Intent Overhaul
Status: ✅ IMPLEMENTED
Prerequisites: None (self-contained prompt changes)
Objective: Give each platform group a distinct content intent so generated posts serve their actual purpose — social drives organic growth, email informs subscribers, blog targets search queries.
Background
Currently all platforms receive the same context-enriched prompt via synthesizeTopic(), regardless of whether the output will be a Tweet, a newsletter, or a blog post. The generators differ only in formatting instructions, not strategic intent. This means:
- Social posts read like product announcements, not community content
- Email content is generic feature promotion, not subscriber value
- Blog posts lack SEO structure and are too short (400–600 words) for search ranking
7.1 — Social Generator Prompt Rewrites
Target files: api/src/agents/createPosts.ts
Platforms: facebook, instagram, twitter, linkedin, tiktok
What changes
Replace the current "call-to-action" framing (which defaults to "learn more") with organic growth framing. The goal shifts from driving traffic to driving engagement signals (comments, shares, saves, follows) that grow reach.
Per-platform changes
Facebook — add to prompt:
This post's goal is organic reach and community engagement, not direct promotion.
- Spark a conversation: ask a question the audience genuinely wants to answer
- Use storytelling or a relatable observation related to the topic
- CTA should be low-friction: "What do you think?", "Drop a comment below", "Share this with someone who…"
- Avoid "learn more" / "click here" / "buy now" CTAs
Instagram — add to prompt:
This post's goal is organic discovery and saves/shares, not direct promotion.
- Open with a hook that stops the scroll in the first line
- Make it save-worthy: a tip, insight, or relatable moment the audience will want to revisit
- CTA should be engagement-focused: "Save this for later", "Tag someone who needs this", "Share your experience in the comments"
- Avoid "link in bio for X" as the primary CTA
Twitter/X — add to prompt:
This post's goal is organic reach through replies and retweets, not direct promotion.
- Start with the most provocative or insightful statement — no warm-up
- Invite a reaction or debate: a bold take, a surprising stat, or a question
- CTA: "What's your take?", "Agree or disagree?", "Reply with your experience"
- Avoid "check out our…" CTAs
LinkedIn — add to prompt:
This post's goal is professional reach and industry credibility, not direct promotion.
- Lead with a lesson, insight, or industry observation — not a product feature
- Tell a short story or share a pattern noticed from experience
- CTA should invite peer discussion: "Has your team experienced this?", "What's worked for you?", "Curious what others think"
- Avoid "follow us for more content" as the primary CTA
TikTok — add to prompt:
This post's goal is algorithmic reach through watch-time and saves, not direct promotion.
- The first line must be a hook that creates curiosity or FOMO
- Structure as a "did you know" / "here's why" / "stop doing X" format where possible
- CTA should drive follows or saves: "Follow for more", "Save this if you…", "Part 2 if this gets 50 comments"
- Avoid self-promotional language in the hook
Tasks
- [x] Update
createFacebookPost()prompt inapi/src/agents/createPosts.ts - [x] Update
createInstagramPost()prompt - [x] Update
createTwitterPost()prompt (preserve 280-char enforcement logic) - [x] Update
createLinkedInPost()prompt - [x] Update
createTikTokPost()prompt
7.2 — Blog Generator Prompt Rewrite
Target files: api/src/agents/createPosts.ts
Platforms: blog, ghost, wordpress, blogger
What changes
Restructure createBlogPost() around search intent rather than product promotion. Target word count increases from 400–600 to 800–1200 words to meet minimum SEO thresholds for competitive ranking.
Updated prompt structure
Write an SEO-optimised blog post for {company_name} ({website}).
Company: {company_description}
Topic/Brief: {feature_description}
{brand_voice}
SEO requirements:
- H1 title: must be a search query or closely match what a person would type into Google
- H2 headings: structure as questions (e.g. "What is…?", "How do you…?", "Why does…?")
- Length: 800–1,200 words
- Include a <meta name="description"> tag (150–160 characters) summarising the post for search results
- Naturally include semantically related terms throughout (do not keyword-stuff)
- Write for a reader who found this post via a search query — they want a clear answer, not a sales pitch
Structure:
1. H1 — the search query / primary topic (include primary keyword)
2. Introduction — answer the main question in 2–3 sentences, then expand
3. 3–4 H2 sections — each addressing a related sub-question a searcher would have
4. Optional: numbered list or comparison table if the topic suits it
5. Conclusion with a soft, helpful CTA (not a hard sell)
6. 2–3 [IMAGE: ...] placeholders at relevant points
Return the full post in valid HTML (h1, h2, h3, p, ul/ol, table if used, meta description tag).
Wrap the HTML in triple backticks.
Write 1 post only. No explanations outside the HTML.
Tasks
- [x] Rewrite
createBlogPost()prompt inapi/src/agents/createPosts.ts - [x] Update word-count validation in
extractBlogHtml()(if it enforces a max — adjust to 1,200; no hard limit existed, prompt now targets 800–1,200 words) - [x] Update
critiqueContent()scoring rubric for blog: add "SEO structure" criterion, penalise H2s that are not question-format when no keywords are provided
7.3 — Email Generator Prompt Rewrite
Target files: api/src/agents/createPosts.ts, api/src/utils/smart-mapper.ts
What changes
Shift the framing in generateContentForRoles() from "promoting a feature" to "informing subscribers about something valuable". This is a prompt-level change — no new data sources yet (those come in Phase 8).
Updated role generation context
In generateContentForRoles(), update the base system framing passed to each role's LLM call:
You are writing content for a marketing email sent to an existing subscriber list.
The goal is to inform and provide value — NOT to sell or promote.
Subscribers have already opted in; they want industry news, product updates, useful tips,
and insights relevant to their work or interests.
Do NOT use:
- "Check out our latest…"
- "We're excited to announce…" (use sparingly, only for genuinely significant news)
- Generic calls-to-action like "Learn more" or "Click here"
DO use:
- Specific, concrete details about what has changed, improved, or is new
- Industry context that makes the update feel timely and relevant
- CTAs that deliver clear value: "Read the full guide", "Try it now", "See what's new"
Tasks
- [x] Update the base framing in
generateContentForRoles()inapi/src/utils/smart-mapper.ts - [x] Update
HERO_HEADLINErole prompt to request a subject-line-style headline (not a campaign tagline) - [x] Update
CTA_BUTTON/CTA_LINKrole prompt to require value-specific CTAs - [x] Update the legacy
createEmailTemplate()step-1 prompt (used as fallback)
7.4 — Platform-Group Intent in synthesizeTopic()
Target file: api/src/modules/auto-generate-context.ts
What changes
synthesizeTopic() currently produces one prompt for all platforms in a generation run. Add platform-group detection and inject a strategic framing block that aligns the synthesised topic with the platform group's intent.
Platform group detection
function detectPlatformGroup(platforms: string[]): 'social' | 'email' | 'blog' | 'mixed' {
const socialPlatforms = new Set(['facebook', 'instagram', 'twitter', 'linkedin', 'tiktok'])
const blogPlatforms = new Set(['blog', 'ghost', 'wordpress', 'blogger'])
const emailPlatforms = new Set(['email'])
const hasSocial = platforms.some((p) => socialPlatforms.has(p))
const hasBlog = platforms.some((p) => blogPlatforms.has(p))
const hasEmail = platforms.some((p) => emailPlatforms.has(p))
const groupCount = [hasSocial, hasBlog, hasEmail].filter(Boolean).length
if (groupCount > 1) return 'mixed'
if (hasSocial) return 'social'
if (hasBlog) return 'blog'
if (hasEmail) return 'email'
return 'mixed'
}
Framing blocks to inject into synthesizeTopic() prompt
const platformGroupFraming: Record<string, string> = {
social: `
CONTENT GOAL — ORGANIC GROWTH:
The content you suggest must be designed to earn engagement (comments, shares, saves, follows),
not to drive direct traffic or sales. Suggest topics that spark conversation or genuine curiosity.
Avoid product-announcement angles.`,
email: `
CONTENT GOAL — SUBSCRIBER VALUE:
The content you suggest must inform and provide genuine value to existing subscribers.
Suggest topics that feel timely, specific, and useful — industry news, product updates,
practical tips, or notable changes relevant to the company's niche.
Avoid generic promotional angles.`,
blog: `
CONTENT GOAL — ORGANIC SEARCH:
The content you suggest must target a question or search query a potential customer would type
into Google or an AI assistant. Suggest a specific topic phrased as a search intent
(e.g. "how to X", "best Y for Z", "why does W happen"). Avoid promotional angles.`,
mixed: `
CONTENT GOAL — MULTI-CHANNEL:
The content you suggest will be adapted across multiple channel types. Suggest a topic that has
genuine value for existing subscribers (email), is discoverable via search (blog), and can spark
engagement on social. Prioritise informational/educational angles over promotional ones.`,
}
Tasks
- [x] Add
detectPlatformGroup()helper function toauto-generate-context.ts - [x] Update
synthesizeTopic()signature to acceptplatforms: string[] - [x] Inject the appropriate framing block into the LLM synthesis prompt before the topic-selection instruction
- [x] Update
runSingleGeneration()to passplatformsthrough tosynthesizeTopic() - [x] Add a brief log line so server logs show which platform group was detected per run
Testing Checklist
- [ ] Generate a Facebook post manually via the dashboard — verify CTA is engagement-focused (not "learn more")
- [ ] Generate a LinkedIn post — verify it leads with an insight, not a product feature
- [ ] Generate a blog post — verify it contains H2s phrased as questions and
<meta name="description"> - [ ] Generate a blog post — verify word count is 800–1,200 words
- [ ] Generate an email via
POST /generate_post— verify headline is subject-line style and CTA is value-specific - [ ] Trigger
POST /plan_postswith a customer that has social + blog platforms — confirm server logs show distinct platform groups - [ ] Run unit tests:
cd api && pnpm test:routes— no regressions
Rollback
All changes are prompt-only (no schema, no new services). To revert: restore original prompt strings in createPosts.ts, revert framing in smart-mapper.ts, and remove detectPlatformGroup call from synthesizeTopic.