AI 封面图生成
1339 字
7 分钟
AI 封面图生成

文章摘要
方案说明
手动为每篇文章找封面图很费时间,本方案通过 AI 图片生成 API 自动生成封面图:
- 读取文章的
title、description、tags,生成中文提示词 - 调用图片生成 API(如 DALL-E、Stable Diffusion 等)
- 将生成的图片保存到
public/images/covers/generated/ - 自动更新文章 frontmatter 的
image字段 - 支持批量生成或指定单篇文章生成
配置 .env
文件路径:.env
# AI 封面图生成配置(仅本地生成时使用,不需要提交,也不需要配到部署平台)AI_COVER_API_KEY=sk-xxxxxxxxxxxxxxxxAI_COVER_API_URL=https://api.openai.com/v1AI_COVER_MODEL=dall-e-3.env 已在 .gitignore 中,不会被提交。支持 OpenAI 原生 API 和兼容的第三方代理。
编写生成脚本
文件路径:scripts/generate-cover-images.ts
导入依赖
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";import { join } from "node:path";import { config } from "dotenv";
config();
const BLOG_DIR = "src/content/blog";const OUTPUT_DIR = "public/images/covers/generated";const API_KEY = process.env.AI_COVER_API_KEY;const API_URL = process.env.AI_COVER_API_URL;const MODEL = process.env.AI_COVER_MODEL || "dall-e-3";解析 Frontmatter
function parseFrontmatter(content: string) { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return null;
const frontmatter: Record<string, any> = {}; const lines = match[1].split("\n");
for (const line of lines) { const [key, ...valueParts] = line.split(":"); if (key && valueParts.length) { const value = valueParts.join(":").trim(); frontmatter[key.trim()] = value.replace(/^["']|["']$/g, ""); } }
return frontmatter;}生成提示词
根据文章元数据生成图片生成提示词:
function generatePrompt(frontmatter: any): string { const title = frontmatter.title || ""; const description = frontmatter.description || ""; const tags = frontmatter.tags || "";
return `为技术博客文章创建一个简洁、现代的封面图片。文章标题:${title}。描述:${description}。标签:${tags}。风格要求:扁平化设计、渐变色背景、图标或抽象元素、适合作为博客封面。`;}调用 API 生成图片
使用 fetch 直接调用图片生成 API,接收 base64 格式图片:
async function generateImage(prompt: string, filename: string): Promise<string> { console.log(`🎨 正在生成图片:${filename}`);
const response = await fetch(`${API_URL}/images/generations`, { method: "POST", headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: MODEL, prompt, n: 1, size: "1792x1024", response_format: "b64_json", }), });
if (!response.ok) { const errorText = await response.text(); throw new Error(`API 请求失败 (${response.status}): ${errorText}`); }
const data = await response.json(); const b64Data = data.data?.[0]?.b64_json; if (!b64Data) throw new Error("生成图片失败");
const imageBuffer = Buffer.from(b64Data, "base64"); const outputPath = join(OUTPUT_DIR, filename);
await mkdir(OUTPUT_DIR, { recursive: true }); await writeFile(outputPath, imageBuffer); console.log(`✅ 已保存:${outputPath}`);
return `/images/covers/generated/${filename}`;}处理单个文章
async function processFile(file: string) { const filePath = join(BLOG_DIR, file); const content = await readFile(filePath, "utf-8"); const frontmatter = parseFrontmatter(content);
if (!frontmatter) { console.log(`⚠️ 跳过(无 frontmatter):${file}`); return; }
// 跳过已有封面的文章(非 api 且非 unsplash) if (frontmatter.image && frontmatter.image !== "api" && !frontmatter.image.includes("source.unsplash.com")) { console.log(`⏭️ 跳过(已有封面):${file}`); return; }
const prompt = generatePrompt(frontmatter); const filename = `${file.replace(/\.(md|mdx)$/, "")}.png`.replace(/\//g, "-"); const imagePath = await generateImage(prompt, filename);
// 更新 frontmatter 的 image 字段 const newContent = content.replace( /^(---\n[\s\S]*?)image:.*$/m, `$1image: ${imagePath}`, );
if (newContent === content) { // 如果没有 image 字段,追加一个 const updatedContent = content.replace( /^(---\n[\s\S]*?)\n---/, `$1\nimage: ${imagePath}\n---`, ); await writeFile(filePath, updatedContent); } else { await writeFile(filePath, newContent); }
console.log(`✨ 已更新文章:${file}\n`);}主函数
支持两种模式:批量生成所有文章,或指定单篇文章:
async function main() { const targetFile = process.argv[2];
if (targetFile) { // 单文章模式 const relativePath = targetFile .replace(/\\/g, "/") .replace(/^src\/content\/blog\//, ""); console.log(`📝 处理单个文章:${relativePath}\n`); try { await processFile(relativePath); } catch (error) { console.error(`❌ 处理失败:${relativePath}`, error); process.exit(1); } } else { // 批量模式 const files = await readdir(BLOG_DIR, { recursive: true }); const mdFiles = files.filter((f) => f.endsWith(".md") || f.endsWith(".mdx"));
console.log(`📝 找到 ${mdFiles.length} 篇文章\n`);
for (const file of mdFiles) { try { await processFile(file); } catch (error) { console.error(`❌ 处理失败:${file}`, error); } } }
console.log("\n🎉 全部完成!");}
main();注册命令
文件路径:package.json
"scripts": { "cover-images": "npx tsx scripts/generate-cover-images.ts"}使用方法
批量生成所有文章封面
pnpm cover-images会扫描 src/content/blog/ 下所有文章,跳过已有封面的文章,为其他文章生成封面图。
为单篇文章生成封面
pnpm cover-images src/content/blog/xxx.md只为指定文章生成封面图,即使它已有封面也会重新生成(除非是本地路径的封面)。
完整工作流
# 1. 写好文章,frontmatter 的 image 设为 "api"# 2. 生成封面图pnpm cover-images src/content/blog/新文章.md
# 3. 提交文章和生成的图片git add src/content/blog/ public/images/covers/generated/
# 4. 推送git push跳过逻辑
脚本会自动跳过以下情况:
- 无 frontmatter - 不是标准的 Markdown 文章
- 已有本地封面 -
image字段已设置且不是api或 unsplash 随机图 - 处理失败 - API 调用失败时保留原文件,继续处理下一篇
如果想强制重新生成,可以先把 frontmatter 的 image 改为 api,再运行命令。
成本优化建议
图片生成 API 通常按次数计费,建议:
- 精准生成 - 用单文章模式,只为需要的文章生成
- 提示词优化 - 修改
generatePrompt()函数,生成更符合需求的提示词 - 使用代理 - 使用第三方 API 代理服务,成本更低
- 批量提交 - 一次性生成多篇文章的封面后再提交,避免频繁调用
注意事项
- API 限流 - 批量生成时注意 API 限流,必要时添加延迟
- 图片质量 - DALL-E 3 生成质量较高但成本也高,可根据需求选择其他模型
- 提示词语言 - 当前使用中文提示词,部分 API 可能需要改为英文
- 文件命名 - 生成的文件名会替换路径分隔符为
-,避免子目录问题
扩展思路
- 自定义提示词模板 - 根据分类或标签使用不同风格的提示词
- 图片尺寸配置 - 支持配置不同的图片尺寸
- 本地模型 - 接入 Stable Diffusion 本地模型,完全免费
- 审核机制 - 生成后人工审核,不满意的可以重新生成
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


