AI 封面图生成

1339 字
7 分钟
AI 封面图生成
文章摘要

方案说明#

手动为每篇文章找封面图很费时间,本方案通过 AI 图片生成 API 自动生成封面图:

  • 读取文章的 titledescriptiontags,生成中文提示词
  • 调用图片生成 API(如 DALL-E、Stable Diffusion 等)
  • 将生成的图片保存到 public/images/covers/generated/
  • 自动更新文章 frontmatter 的 image 字段
  • 支持批量生成或指定单篇文章生成

配置 .env#

文件路径:.env

Terminal window
# AI 封面图生成配置(仅本地生成时使用,不需要提交,也不需要配到部署平台)
AI_COVER_API_KEY=sk-xxxxxxxxxxxxxxxx
AI_COVER_API_URL=https://api.openai.com/v1
AI_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"
}

使用方法#

批量生成所有文章封面#

Terminal window
pnpm cover-images

会扫描 src/content/blog/ 下所有文章,跳过已有封面的文章,为其他文章生成封面图。

为单篇文章生成封面#

Terminal window
pnpm cover-images src/content/blog/xxx.md

只为指定文章生成封面图,即使它已有封面也会重新生成(除非是本地路径的封面)。

完整工作流#

Terminal window
# 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

跳过逻辑#

脚本会自动跳过以下情况:

  1. 无 frontmatter - 不是标准的 Markdown 文章
  2. 已有本地封面 - image 字段已设置且不是 api 或 unsplash 随机图
  3. 处理失败 - API 调用失败时保留原文件,继续处理下一篇

如果想强制重新生成,可以先把 frontmatter 的 image 改为 api,再运行命令。

成本优化建议#

图片生成 API 通常按次数计费,建议:

  1. 精准生成 - 用单文章模式,只为需要的文章生成
  2. 提示词优化 - 修改 generatePrompt() 函数,生成更符合需求的提示词
  3. 使用代理 - 使用第三方 API 代理服务,成本更低
  4. 批量提交 - 一次性生成多篇文章的封面后再提交,避免频繁调用

注意事项#

  1. API 限流 - 批量生成时注意 API 限流,必要时添加延迟
  2. 图片质量 - DALL-E 3 生成质量较高但成本也高,可根据需求选择其他模型
  3. 提示词语言 - 当前使用中文提示词,部分 API 可能需要改为英文
  4. 文件命名 - 生成的文件名会替换路径分隔符为 -,避免子目录问题

扩展思路#

  • 自定义提示词模板 - 根据分类或标签使用不同风格的提示词
  • 图片尺寸配置 - 支持配置不同的图片尺寸
  • 本地模型 - 接入 Stable Diffusion 本地模型,完全免费
  • 审核机制 - 生成后人工审核,不满意的可以重新生成

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

AI 封面图生成
https://astro.cchaoka.cn/blog/AI封面图生成/
作者
像风
发布于
2026-06-12
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
像风
Hello, I'm 像风.
📢 欢迎来访者
👋🏻 Hi,我是像风,欢迎您!
分类
标签
站点统计
文章
12
分类
2
标签
3
总字数
5,056
运行时长
0
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.14.3
文章许可
CC BY-NC-SA 4.0
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
✨ 今日一言
"做你自己,别人已经有人做了。"
—— 奥斯卡·王尔德
统计
✨️ 复制成功,转载请标注本文地址