create-split
Version:
CLI tool to connect Next.js sites to Split AI content delivery system
173 lines (147 loc) • 5.61 kB
text/typescript
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
/**
* Webhook handler for Split AI content delivery
* This endpoint receives content from Split and saves it to your designated content directory
*/
export async function POST(req: NextRequest) {
try {
// 1. Verify the request signature
const signature = req.headers.get('x-split-signature');
if (!signature) {
return new NextResponse(JSON.stringify({ error: 'Missing signature' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
// 2. Get request body as text for signature verification
const rawBody = await req.text();
// 3. Verify HMAC signature
const isValid = verifySignature(rawBody, signature);
if (!isValid) {
return new NextResponse(JSON.stringify({ error: 'Invalid signature' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
// 4. Parse the body content
const body = JSON.parse(rawBody);
const { content, metadata } = body;
if (!content || !metadata || !metadata.slug) {
return new NextResponse(JSON.stringify({ error: 'Missing required fields' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// 5. Save the MDX content to the content directory
const contentDir = process.env.SPLIT_CONTENT_DIR || 'content/split';
const filePath = path.join(process.cwd(), contentDir, `${metadata.slug}.mdx`);
// Ensure the directory exists
await fs.mkdir(path.dirname(filePath), { recursive: true });
// Write the file
await fs.writeFile(filePath, content);
// 6. Optionally update sitemap.xml and llms.txt
await updateSitemap(metadata);
await updateLlmsTxt(metadata);
return new NextResponse(JSON.stringify({ success: true, slug: metadata.slug }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
console.error('Split agent error:', error);
return new NextResponse(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
/**
* Verify the HMAC signature from Split
*/
function verifySignature(payload: string, signature: string): boolean {
try {
const secret = process.env.SPLIT_AGENT_SECRET;
if (!secret) {
console.error('Missing SPLIT_AGENT_SECRET environment variable');
return false;
}
const hmac = crypto.createHmac('sha256', secret);
const expectedSignature = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
/**
* Update sitemap.xml with the new content URL
*/
async function updateSitemap(metadata: { slug: string, title: string }) {
try {
const sitemapPath = path.join(process.cwd(), 'public', 'sitemap.xml');
// Check if sitemap exists
let sitemapContent: string;
try {
sitemapContent = await fs.readFile(sitemapPath, 'utf8');
} catch (error) {
console.warn('Sitemap not found, skipping update');
return;
}
// Don't add duplicate entries
if (sitemapContent.includes(`<loc>https://${process.env.NEXT_PUBLIC_DOMAIN}/blog/${metadata.slug}</loc>`)) {
return;
}
// Add new URL before the closing tag
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const newEntry = ` <url>
<loc>https://${process.env.NEXT_PUBLIC_DOMAIN || process.env.VERCEL_URL || 'yourdomain.com'}/blog/${metadata.slug}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- Split AI content will be automatically added to this sitemap -->`;
// Replace the comment with our new entry plus the comment again (to keep the marker for future additions)
sitemapContent = sitemapContent.replace(
'<!-- Split AI content will be automatically added to this sitemap -->',
newEntry
);
await fs.writeFile(sitemapPath, sitemapContent);
console.log(`Updated sitemap.xml with new entry for slug: ${metadata.slug}`);
} catch (error) {
console.error('Error updating sitemap:', error);
}
}
/**
* Update llms.txt with the new content URL
*/
async function updateLlmsTxt(metadata: { slug: string, title: string }) {
try {
const llmsPath = path.join(process.cwd(), 'public', 'llms.txt');
// Check if llms.txt exists
let llmsContent: string;
try {
llmsContent = await fs.readFile(llmsPath, 'utf8');
} catch (error) {
console.warn('llms.txt not found, skipping update');
return;
}
// Don't add duplicate entries
const domain = process.env.NEXT_PUBLIC_DOMAIN || process.env.VERCEL_URL || 'yourdomain.com';
const urlToAdd = `https://${domain}/blog/${metadata.slug}`;
if (llmsContent.includes(urlToAdd)) {
return;
}
// Add new URL with a comment describing the content
const newEntry = `${urlToAdd} # ${metadata.title}`;
llmsContent += `\n${newEntry}`;
await fs.writeFile(llmsPath, llmsContent);
console.log(`Updated llms.txt with new entry for slug: ${metadata.slug}`);
} catch (error) {
console.error('Error updating llms.txt:', error);
}
}