OpenCode 实践课:4 分钟用 Python 写出一个 Markdown 静态站点生成器

项目介绍

有时候我们写了几篇 Markdown 笔记或文档,想快速生成一个带导航的静态网站,又不想用 Hugo、Hexo 这些重型框架。今天我们就用 OpenCode 命令行 AI 助手,4 分钟内写一个 Markdown 静态站点生成器——把 .md 文件批量转换成 HTML 页面,自动生成首页目录,一行命令搞定。

  • 技术栈:Python 3 + markdown
  • 最终代码:约 110 行
  • 效果python ssg.py src/ dist/ → 得到一套精美的 HTML 站点

准备工作

安装 OpenCode 后,确保有 Python 3 环境。依赖只需要一个 markdown 包:

pip install markdown

然后打开终端,进入项目目录,开始对 OpenCode 下达指令。

实践过程

第一步:搭建项目骨架

我直接对 OpenCode 说:

> 帮我创建一个 Python 脚本 ssg.py,先搭好基本框架:读取命令行参数 src 和 dist 目录,扫描 src 下所有 .md 文件。

OpenCode 立刻生成了入口代码:

import sys
from pathlib import Path

def main():
    src_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('src')
    dst_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path('dist')

    if not src_dir.exists():
        print(f'错误: 源目录 "{src_dir}" 不存在')
        sys.exit(1)

    md_files = list(src_dir.glob('**/*.md'))
    print(f'找到 {len(md_files)} 个 Markdown 文件')

if __name__ == '__main__':
    main()

要点pathlib.Path.glob('**/*.md')os.walk 更简洁,递归扫描所有子目录。OpenCode 自动选择了现代 Python 写法,不需要我额外指定。

第二步:实现 Markdown 到 HTML 转换

我接着对它说:

> 加入 HTML 模板和 markdown 转换逻辑,用 markdown 库把每个 .md 转为 HTML 页面。

OpenCode 补充了核心转换函数和 HTML 模板:

import markdown

HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
nav { border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 30px; }
nav a { margin-right: 15px; color: #0366d6; text-decoration: none; }
pre { background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }
code { background: #f6f8fa; padding: 2px 6px; border-radius: 3px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px 12px; }
</style>
</head>
<body>
<nav><a href="/index.html">首页</a></nav>
<main>{{content}}</main>
</body>
</html>"""


def convert_file(src: Path, dst: Path):
    content = src.read_text(encoding='utf-8')
    html_body = markdown.markdown(content, extensions=['fenced_code', 'tables'])
    title = src.stem
    html = HTML_TEMPLATE.replace('{{title}}', title).replace('{{content}}', html_body)
    dst.parent.mkdir(parents=True, exist_ok=True)
    dst.write_text(html, encoding='utf-8')
    return title, dst

要点markdown.markdown()extensions 参数很关键——fenced_code 支持围栏代码块,tables 支持 GFM 表格。OpenCode 自动加上了这两个常用扩展,比手写正则靠谱得多。

第三步:生成首页目录

我对 OpenCode 说:

> 再生成一个 index.html 首页,列出所有文章的链接。

OpenCode 给出了索引页的生成逻辑:

INDEX_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>站点目录</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { border-bottom: 2px solid #0366d6; padding-bottom: 10px; }
ul { list-style: none; padding: 0; }
li { margin: 12px 0; }
a { color: #0366d6; text-decoration: none; font-size: 1.1em; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>文章列表</h1>
<ul>{{links}}</ul>
</body>
</html>"""


def generate_index(files, output_dir: Path):
    links = []
    for title, path in files:
        rel_path = path.relative_to(output_dir).as_posix()
        links.append(f'<li><a href="/{rel_path}">{title}</a></li>')
    html = INDEX_TEMPLATE.replace('{{links}}', '\n'.join(links))
    (output_dir / 'index.html').write_text(html, encoding='utf-8')

要点:首页目录用 path.relative_to().as_posix() 生成跨平台的相对路径,保证 Windows 和 Linux 下都能正常访问。

第四步:串联主流程并测试

最后我对 OpenCode 说:

> 把上面的函数串起来,在 main 里调用 convert_file 和 generate_index。

OpenCode 完成了主函数,我去终端测试:

# 创建测试目录和一篇示例文章
mkdir src
echo '# Hello World

这是一篇测试文章。

print("Hello, OpenCode!")

# 运行生成器
python ssg.py src dist

输出:

✓ src\hello.md → dist\hello.html

完成! 共转换 1 个文件,输出目录: dist

用浏览器打开 dist/index.html,首页上出现了文章链接,点击进入后代码高亮、表格样式都正常渲染。

完整代码

#!/usr/bin/env python3
import sys
from pathlib import Path
import markdown

HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
nav { border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 30px; }
nav a { margin-right: 15px; color: #0366d6; text-decoration: none; }
pre { background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }
code { background: #f6f8fa; padding: 2px 6px; border-radius: 3px; }
table { border-collapse: collapse; width: 100%; margin: 15px 0; }
th, td { border: 1px solid #ddd; padding: 8px 12px; }
th { background: #f6f8fa; }
blockquote { border-left: 4px solid #0366d6; padding-left: 15px; color: #666; }
</style>
</head>
<body>
<nav><a href="/index.html">首页</a></nav>
<main>{{content}}</main>
</body>
</html>"""

INDEX_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>站点目录</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
h1 { border-bottom: 2px solid #0366d6; padding-bottom: 10px; }
ul { list-style: none; padding: 0; }
li { margin: 12px 0; }
a { color: #0366d6; text-decoration: none; font-size: 1.1em; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>文章列表</h1>
<ul>{{links}}</ul>
</body>
</html>"""


def convert_file(src: Path, dst: Path):
    content = src.read_text(encoding='utf-8')
    html_body = markdown.markdown(content, extensions=['fenced_code', 'tables'])
    title = src.stem
    html = HTML_TEMPLATE.replace('{{title}}', title).replace('{{content}}', html_body)
    dst.parent.mkdir(parents=True, exist_ok=True)
    dst.write_text(html, encoding='utf-8')
    return title, dst


def generate_index(files, output_dir: Path):
    links = []
    for title, path in files:
        rel_path = path.relative_to(output_dir).as_posix()
        links.append(f'<li><a href="/{rel_path}">{title}</a></li>')
    html = INDEX_TEMPLATE.replace('{{links}}', '\n'.join(links))
    (output_dir / 'index.html').write_text(html, encoding='utf-8')


def main():
    src_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('src')
    dst_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path('dist')

    if not src_dir.exists():
        print(f'错误: 源目录 "{src_dir}" 不存在')
        sys.exit(1)

    md_files = list(src_dir.glob('**/*.md'))
    if not md_files:
        print(f'警告: 在 "{src_dir}" 中没有找到 .md 文件')
        sys.exit(0)

    dst_dir.mkdir(parents=True, exist_ok=True)
    results = []

    for md_file in md_files:
        dst = dst_dir / md_file.relative_to(src_dir).with_suffix('.html')
        title, path = convert_file(md_file, dst)
        results.append((title, path))
        print(f'✓ {md_file} → {dst}')

    generate_index(results, dst_dir)
    print(f'\n完成! 共转换 {len(results)} 个文件,输出目录: {dst_dir}')


if __name__ == '__main__':
    main()

小结

全程 4 分钟,我只用了 4 句自然语言指令,OpenCode 自动完成了:

项目骨架搭建

Markdown 到 HTML 的转换(含代码高亮、表格支持)

首页目录生成

完整的错误处理和控制台反馈

相比手动编写,省去了查文档、写样板代码和调试 CSS 的时间。最让我惊喜的是,OpenCode 自动选了 pathlib 而非 os.path、自动加了 fenced_codetables 扩展——这些恰恰是新手容易漏掉的细节。

用 AI 编程助手开发工具类的项目,效率提升非常明显。你的角色从"码农"变成了"产品经理"——想清楚要什么,剩下的交给 AI。