Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions kits/automation/blog-automation/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Lamatic Flows
AUTOMATION_BLOG_DRAFTING="AGENTIC_GENERATE_CONTENT FLOW ID"
AUTOMATION_BLOG_SEO="AGENTIC_GENERATE_CONTENT FLOW ID"
AUTOMATION_BLOG_PUBLISH="AGENTIC_GENERATE_CONTENT FLOW ID"

# Lamatic Core
LAMATIC_API_URL="LAMATIC_API_URL"
LAMATIC_PROJECT_ID="LAMATIC_PROJECT_ID"
LAMATIC_API_KEY="LAMATIC_API_KEY"

# WordPress Configuration
WORDPRESS_SITE_ID="YOUR_SITE_ID"
WORDPRESS_TOKEN="YOUR_WORDPRESS_TOKEN"
34 changes: 34 additions & 0 deletions kits/automation/blog-automation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
62 changes: 62 additions & 0 deletions kits/automation/blog-automation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Blog Writing Automation Agent Kit

<p align="center">
<img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExdGhrdHE0Ymh1OXJ3YjR6aHZ1Z2locG9oOXRzam94MDRsbnZyM3o3ZSZlcD12MV9faW50ZXJuYWxfZ2lmX2J5X2lkJmN0PWc/gleoRKw65bDoBOAv6S/giphy.gif" alt="Demo" />
</p>

**Blog Writing Automation** is an AI-powered tool built with [Lamatic.ai](https://lamatic.ai) that automates the generation and publishing of blog posts. It can be triggered externally via webhooks or scheduled tasks to maintain a consistent content pipeline.

---

## 🛠️ How It Works (Step-by-Step)

1. **External Trigger**: An external webhook (e.g., from CRM, Zapier, or a scheduler) signals the agentkit to start a new blog post.
2. **Payload Extraction**: The agent fetches the topic, target keywords, and stylistic instructions from the trigger payload.
3. **AI Drafting & SEO**: The agent drafts a blog post using AI, ensuring deep SEO optimization, coherence, and technical accuracy.
4. **Review Phase**: The draft can be reviewed (optionally by a human or another AI agent) before being finalized for publishing.
5. **Multi-Platform Publishing**: The post is automatically published to a CMS (WordPress, Ghost, etc.) or a static blog platform via API.
6. **Status Monitoring**: Logs and execution status of the publishing pipeline are maintained and visible in the dashboard.

---

## 🔑 Setup

### 1. Lamatic Flows
Before running this project, you must build and deploy the following flows in Lamatic:
- **Drafting Flow**: Input (topic, keywords) -> Output (`content` or `draft`).
- **SEO Flow**: Input (draft, keywords) -> Output (`optimized_content` or `content`).
- **Publish Flow**: Input (content, title) -> Output (`publish_status`, `url`).

### 2. Environment Variables
Create a `.env` file in this directory and set the following keys:

```bash
# Lamatic Flow IDs
AUTOMATION_BLOG_DRAFTING = "FLOW_ID_HERE"
AUTOMATION_BLOG_SEO = "FLOW_ID_HERE"
AUTOMATION_BLOG_PUBLISH = "FLOW_ID_HERE"

# Lamatic Connection
LAMATIC_API_URL = "https://api.lamatic.ai" # Or your project-specific GraphQL endpoint
LAMATIC_PROJECT_ID = "YOUR_PROJECT_ID"
LAMATIC_API_KEY = "YOUR_API_KEY"
```

### 3. Install & Run
```bash
npm install
npm run dev
```

---

## 📂 Repo Structure
- `/actions/orchestrate.ts`: Handles the multi-step orchestration logic and field mapping.
- `/app/page.tsx`: Premium dashboard for manual triggers and status monitoring.
- `/orchestrate.js`: Configuration for flow dependencies and schemas.
- `/config.json`: Metadata for the AgentKit repository.

---

## 🤝 Contributing
Refer to the main [CONTRIBUTING.md](../../../CONTRIBUTING.md) for global standards and coding patterns.
133 changes: 133 additions & 0 deletions kits/automation/blog-automation/actions/orchestrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use server"

import { lamaticClient } from "@/lib/lamatic-client"
import { config } from "../orchestrate"

export type BlogAutomationResult = {
success: boolean
draft?: string
optimizedContent?: string
url?: string
error?: string
}

export async function runBlogAutomation(
topic: string,
keywords: string,
instructions: string
): Promise<BlogAutomationResult> {
try {
const { flows } = config

// MOCK MODE: For testing without live Lamatic keys
if (process.env.NEXT_PUBLIC_MOCK_MODE === "true") {
console.log("[Blog Automation] Running in MOCK MODE")
await new Promise(resolve => setTimeout(resolve, 2000)) // Simulate network lag
return {
success: true,
draft: "This is a mock draft for: " + topic,
optimizedContent: "# " + topic + "\n\nThis is optimized content with keywords: " + keywords + "\n\nGenerated with Blog Automation Kit.",
url: "https://example.com/mock-blog-post"
}
}

// 1. Drafting Phase
console.log("[Blog Automation] Starting Drafting...")
const draftingFlow = flows.drafting
if (!draftingFlow.workflowId) throw new Error("Drafting Flow ID missing")

const payload = {
topic,
keywords,
instructions,
// Common variations to ensure mapping
Topic: topic,
Keywords: keywords,
Instructions: instructions
}
console.log("[Blog Automation] Sending Payload to Drafting:", JSON.stringify(payload, null, 2))

const draftingRes = await lamaticClient.executeFlow(draftingFlow.workflowId, payload)
console.log("[Blog Automation] Drafting Response:", JSON.stringify(draftingRes, null, 2))

if (draftingRes?.status === "error") {
throw new Error(`Lamatic Error: ${draftingRes.message || "Unknown error"}`)
}

const draft = draftingRes?.result?.generatedResponse || draftingRes?.result?.content || draftingRes?.result?.draft
if (!draft) throw new Error("Drafting failed: No content generated")

// 2. SEO Optimization Phase
const seoFlow = flows.seo
if (!seoFlow.workflowId) throw new Error("SEO Flow ID missing")

const seoPayload = {
draft,
keywords,
// Common variations to ensure mapping
content: draft,
text: draft,
Keywords: keywords
}
console.log("[Blog Automation] Sending Payload to SEO:", JSON.stringify(seoPayload, null, 2))

const seoRes = await lamaticClient.executeFlow(seoFlow.workflowId, seoPayload)
console.log("[Blog Automation] SEO Response:", JSON.stringify(seoRes, null, 2))

if (seoRes?.status === "error") {
throw new Error(`Lamatic SEO Error: ${seoRes.message || "Unknown error"}`)
}
const optimizedContent = seoRes?.result?.generatedResponse || seoRes?.result?.optimized_content || seoRes?.result?.content || seoRes?.result?.text
if (!optimizedContent) throw new Error("SEO Optimization failed")

// 3. Publishing Phase
console.log("[Blog Automation] Starting Publishing...")
const publishFlow = flows.publish
if (!publishFlow.workflowId) throw new Error("Publish Flow ID missing")

const publishPayload = {
content: optimizedContent,
title: topic,
// Common variations to ensure mapping
text: optimizedContent,
Topic: topic,
Title: topic
}
console.log("[Blog Automation] Sending Payload to Publish:", JSON.stringify(publishPayload, null, 2))

const publishRes = await lamaticClient.executeFlow(publishFlow.workflowId, publishPayload)
console.log("[Blog Automation] Publish Response:", JSON.stringify(publishRes, null, 2))

if (publishRes?.status === "error") {
throw new Error(`Lamatic Publish Error: ${publishRes.message || "Unknown error"}`)
}

const url = publishRes?.result?.url || publishRes?.result?.post_url || publishRes?.result?.link || ""
const rawStatus = publishRes?.result?.publish_status || publishRes?.result?.status || publishRes?.status
const status = (rawStatus === "success" || rawStatus === "publish") ? "success" : rawStatus
const message = publishRes?.result?.message || ""

// Detect if the response is an HTML error page (common with WordPress 404s)
if (typeof message === "string" && (message.includes("<!DOCTYPE html>") || message.includes("Page not found"))) {
throw new Error("Publishing failed: The CMS returned a 'Page Not Found' (404) error. Please check your CMS endpoint in Lamatic Studio.")
}

if (status !== "success" && !url) {
throw new Error(`Publishing failed: Response status was '${rawStatus}' and no URL was found.`)
}

return {
success: true,
draft,
optimizedContent,
url: typeof url === "string" ? url : ""
}

} catch (error) {
console.error("[Blog Automation] Error:", error)
return {
success: false,
error: error instanceof Error ? error.message : "An unexpected error occurred"
}
}
}
125 changes: 125 additions & 0 deletions kits/automation/blog-automation/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
@import 'tailwindcss';
@import 'tw-animate-css';

@custom-variant dark (&:is(.dark *));

:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}

@theme inline {
--font-sans: 'Geist', 'Geist Fallback';
--font-mono: 'Geist Mono', 'Geist Mono Fallback';
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
Loading