9. Use AI to automatically generate content
Members only · Non-members can read 30% of the article.
- Published
- May 17, 2025
- Reading Time
- 8 min read
- Author
- Felix
- Access
- Members only
Non-members can read 30% of the article.
The previous section talked about logging in, and this section will talk about AI automatically generating article content. The core reason why article content needs to be generated: there are too few pages and there is no traffic in Google, and long-term content updates are needed to get traffic.
Create table structure to complete back-end interface
This table structure is added to lib/db/schema.ts to describe published articles.
export const posts = sqliteTable('posts', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
slug: text('slug').notNull().unique(),
title: text('title').notNull(),
excerpt: text('excerpt').notNull(),
content: text('content').notNull(),
locale: text('locale').notNull().default('en'),
publishedAt: integer('published_at', { mode: 'timestamp_ms' }),
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
})
The table structure defined above contains the following fields:
- id: unique identifier of the article, automatically generated using UUID
- slug: URL of the article, used to display in the URL, must be unique
- title: article title, used for MetaTitle
- excerpt: article abstract, used for MetaDescription
- content: The complete content of the article, stored as Markdown
- locale: the language of the article, the default is English ('en'),,
- publishedAt: article publication time, can be null to indicate an unpublished draft
- createdAt: article creation time, automatically set to the current timestamp
- updatedAt: the last updated time of the article, automatically set to the current timestamp
The point worth paying attention to is the language setting. Obviously articles need to be I18n, but there is no need to hard-translate each article into different meanings with AI. The best practice is to generate articles based on keywords from different countries.
Then execute pnpm db:migrate-local to generate the local table, and then complete the back-end Api writing.
Complete the writing of Api
Add the ai-content.ts file in actions, which contains all server-side functions related to AI content generation. A small development habit worth noting is: Split it as detailed as possible and split it according to functions.
The first is generateArticle. This function is the core of our AI article generation. Most of it is conventional logic, but there are some places that we need to pay attention to:
- Regarding the writing method of Prompt, there is a set of most commonly used templates, which uses Markdown structure and hierarchy to express requirements and key points. Role positioning is completed at the beginning and output constraints are completed at the end.
- In most scenarios, it is not required to force the output to JSON. This will affect the degree of freedom and output of the AI, and it will encounter situations where it is difficult to
JSON.Parse. Regular extraction is a better choice. - Pay attention to this priceless reminder:
Please use "keywords" as the keyword to search the first 20 search results on Google, and record their article structure and titles. Then, based on these contents, output an article that conforms to Google SEO logic and user experience.This sentence does not actually search the Internet, but we imagine that the AI data itself is crawled from major search engines. This way of writing prompt words will guide the search vector to output and find answers near the approximate coordinates in its big data. (It’s a bit convoluted. Prompt itself is constantly helping the model to correct coordinates in its big universe. It will be much better to simply understand and write the output like this) typeof analysisResult === 'object'is written to help TS type derivation.- I18n needs to be specially declared in the prompt word in some languages.
- This prompt word has a lot of room for optimization. A better model can include specific SEO best article practices and requirements. This low-parameter model can only generalize prompt words (it does not follow detailed rules).
'use server'
import { desc, eq } from 'drizzle-orm'
import { locales } from '@/i18n/routing'
import { createAI } from '@/lib/ai'
import { createDb } from '@/lib/db'
import { posts } from '@/lib/db/schema'
interface ArticleGenerationParams {
keyword: string
locale?: string
}
interface BatchArticleGenerationParams {
keywords: string[]
locale?: string
}
function getLanguageNameFromLocale(localeCode: string): string {
const locale = locales.find((l) => l.code === localeCode)
if (locale) {
return locale.name
}
return 'English'
}
export async function generateArticle({ keyword, locale = 'en' }: ArticleGenerationParams) {
const languageName = getLanguageNameFromLocale(locale)
const systemPrompt = `
You are an SEO content writer. Your job is write blog post optimized for keyword, title and outline. Please use "keywords" as the keyword to search the first 20 search results on Google, and record their article structure and titles. Then, based on these contents, output an article that conforms to Google SEO logic and user experience.
Format requirements:
- Start with a single H1 title (# Title) that is EXACTLY 50 characters or less
- The title must include the main keyword and be compelling for readers
- Use markdown formatting with proper heading structure (# for H1, ## for H2, etc.)
- Include well-formatted paragraphs, lists, and other elements as appropriate
- Maintain a professional, informative tone
SEO requirements:
- Make the first paragraph suitable for a meta description
- Answer common user questions related to the topic in a conversational tone
- Write in a natural, flowing style that mimics human writing patterns with varied sentence structures
- Avoid obvious AI patterns like excessive lists and formulaic paragraph structures
- Incorporate personal anecdotes, analogies, and relatable examples where appropriate
- Include the most up-to-date information and recent developments on the topic
- Ensure comprehensive coverage with sufficient depth (minimum 1500 words)
Language requirement:
- Write the entire article in ${languageName} language
- Ensure the content is culturally appropriate for ${languageName}-speaking audiences
- Use proper grammar, idioms, and expressions specific to ${languageName}
${locale === 'ar' ? '- Follow right-to-left (RTL) text conventions' : ''}
IMPORTANT: At the very end of your response, include two separate sections:
1. "META_DESCRIPTION:" followed by a concise, SEO-friendly excerpt (130-140 characters max) that includes the main keyword naturally.
2. "URL_SLUG:" followed by an SEO-friendly URL slug for this article (lowercase, words separated by hyphens, no special characters).
Produce original, accurate, and valuable content of at least 10,000 tokens. Output the article content, starting with the H1 title, followed by the meta description and URL slug sections at the end.`
const userPrompt = `Create an article about "${keyword}" in ${languageName} language. Optimize it for search engines while maintaining high-quality, valuable content for readers.`
try {
const cloudflareAI = createAI()
const analysisResult = await cloudflareAI.run('@cf/meta/llama-4-scout-17b-16e-instruct', {
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
stream: false,
max_tokens: 16000
})
if (typeof analysisResult === 'object') {
const fullResponse = analysisResult.response
const metaDescriptionMatch = fullResponse.match(/META_DESCRIPTION:\s*([\s\S]*?)(?=URL_SLUG:|$)/)
const excerpt = metaDescriptionMatch ? metaDescriptionMatch[1].trim() : ''
const urlSlugMatch = fullResponse.match(/URL_SLUG:\s*([\s\S]+)$/)
let slug = urlSlugMatch ? urlSlugMatch[1].trim() : ''
let content = fullResponse
if (metaDescriptionMatch) {
content = content.replace(/META_DESCRIPTION:[\s\S]*$/, '').trim()
}
const titleMatch = content.match(/^#\s+(.+)$/m)
const extractedTitle = titleMatch ? titleMatch[1].trim() : 'Untitled Article'
if (!slug) {
slug = extractedTitle
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
}
return {
title: extractedTitle,
slug,
content,
excerpt: excerpt || content.substring(0, 140) + '...',
locale
}
}
} catch (error) {
throw error
}
}
Subscribe to unlock the full article
Support the writing, unlock every paragraph, and receive future updates instantly.
Comments
Join the conversation
No comments yet. Be the first to add one.