用 GitHub Issues 当数据库,零成本搭一个「提示词归档库」

用 GitHub Issues 当数据库,零成本搭一个「提示词归档库」

没有服务器、没有数据库、没有构建步骤------只用一个 HTML 文件 + GitHub Pages + Issues API,就能上线一个带图片上传、分类筛选、灯箱放大、只读浏览的提示词归档站。本文拆解它的实现思路。

先看成品

打开 codeniu.github.io/prompt-arch... ,你看到的是一个「编辑档案」风格的页面:

  • 顶部固定刊头 + 搜索框 + 新建按钮
  • 分类目录按标签筛选,每标签显示数量
  • 网格卡片布局,每张卡带编号(No. 0001)、标题、正文预览、缩略图、标签
  • 点击卡片进详情,可编辑、复制、删除
  • 支持上传「结果截图」,列表最多展示 3 张,点击放大进灯箱,详情页显示全部
  • 没配 Token 的访客也能只读浏览

整页只有一个 index.html,没有 React、没有 Vue、没有 webpack、没有 npm install。

为什么选 GitHub Issues 当数据库?

做一个小工具,最烦的不是写代码,而是选型与运维

方案 服务器 数据库 成本 痛点
传统全栈 服务器费 要部署、要备份、要续费
BaaS(Supabase 等) 不要 托管 免费额度 要注册、要配表、有迁移成本
纯静态 + localStorage 不要 浏览器 0 数据不跨设备、易丢
GitHub Pages + Issues 不要 Issues 0 天然在 GitHub 上可浏览/搜索/备份

GitHub Issues 本质是一个「带富文本 body、labels、状态、时间戳」的 KV 存储,刚好够存「标题 + 正文 + 标签 + 创建时间」这种结构化不强的内容。而且:

  • 数据即代码资产:issue 就在仓库里,GitHub 自带搜索、订阅、Webhook
  • 零运维:Pages 托管静态文件,Issues 提供 REST API,都不用管
  • 天然版本化:issue 的修订历史 GitHub 都帮你记着
  • 免费且稳定:个人仓库无限 issue,Pages 流量也够用

适合:提示词收藏、读书笔记、灵感速记、个人 linklog 这类轻结构、低并发、个人或小团队的场景。不适合高并发写入或复杂关联查询。

核心映射:Issue 字段 → 提示词

ini 复制代码
issue.title   → 提示词标题
issue.body    → 提示词正文(图片 URL 也藏在 body 里)
issue.labels  → 分类标签
issue.state   → open=存活,closed=已销毁
issue.number  → 档案编号 No.0001
issue.created_at / updated_at → 建立/修订时间

CRUD 直接对应 Issues API:

操作 界面动作 GitHub API
新建 「新建条目」→「归档」 POST /repos/{o}/{r}/issues
查看 点击卡片 GET /repos/{o}/{r}/issues
编辑 详情页「编辑」→「保存修改」 PATCH .../issues/{number}
删除 卡片「销毁」 PATCH .../issues/{number} state=closed

删除是「关闭 issue」而非真删------GitHub API 不支持真删 issue,这反而成了优点:误删可恢复。

难点一:图片往哪存?

提示词经常要配「结果截图」。静态站没有后端,图片怎么传?

答案是 GitHub Contents API :把图片 base64 编码后 PUT 到仓库的 images/ 目录,返回的 download_url 就是可直接热链的图片地址。

js 复制代码
async function ghUploadImage(name, dataUrl) {
  const commaIdx = dataUrl.indexOf(',');
  const base64 = dataUrl.slice(commaIdx + 1);
  const path = `images/${Date.now()}-${rand}.${ext}`;
  const res = await fetch(`${ghBase()}/contents/${path}`, {
    method: 'PUT',
    headers: ghHeaders({ 'Content-Type': 'application/json' }),
    body: JSON.stringify({
      message: `upload image ${path}`,
      content: base64,        // 不含 data: 前缀的纯 base64
    }),
  });
  const data = await res.json();
  return data.download_url;   // raw.githubusercontent.com 直链
}

上传拿到 URL 后,图片 URL 怎么和提示词正文一起存? 我没有新建一张表(也没有表),而是把图片 URL 以 Markdown 图片语法塞进 issue body 末尾,用 HTML 注释做分隔块:

markdown 复制代码
这里是提示词正文......

<!--pa-images-->
![截图](https://raw.githubusercontent.com/.../images/xxx.png)
![截图](https://raw.githubusercontent.com/.../images/yyy.png)
<!--/pa-images-->

读取时用正则把这块切出来:

js 复制代码
const IMG_START = '<!--pa-images-->';
const IMG_END = '<!--/pa-images-->';
const IMG_RE = /!\[[^\]]*\]\((https?:\/\/[^\s)]+)\)/g;

function parseBody(rawBody) {
  const body = rawBody || '';
  const startIdx = body.indexOf(IMG_START);
  if (startIdx === -1) return { content: body, images: [] };
  // ... 切出图片块,正则提 URL,正文去掉这块
}

好处:一次 API 调用同时拿到正文和图片,不用再查仓库目录;issue 在 GitHub 网页端打开也能直接看到图片预览;正文与图片强绑定,不会错配。

难点二:列表最多 3 张,详情显示全部

同一个数据源,两种展示密度。实现上很简单------卡片只取前 3 个 URL,第 3 张如果还有剩余就叠一个 +N 遮罩:

js 复制代码
function renderCardThumbs(number, images) {
  if (!images?.length) return '';
  const show = images.slice(0, 3);
  const extra = images.length - 3;
  return show.map((url, i) => {
    const overlay = (i === 2 && extra > 0)
      ? `<div class="more-overlay">+${extra}</div>` : '';
    return `<div class="card-thumb" onclick="openCardLightbox(${number}, ${i})">
      <img src="${url}" />${overlay}
    </div>`;
  }).join('');
}

点击任意缩略图都打开灯箱 ,支持键盘 ←/→ 切换、Esc 关闭、点背景关闭。灯箱本身就是一个 fixed 全屏遮罩 + 居中大图 + 计数器 1 / 5

js 复制代码
function openLightbox(images, idx) {
  lightboxImgs = images;
  lightboxIdx = Math.max(0, Math.min(idx, images.length - 1));
  $('lightboxImg').src = images[lightboxIdx];
  $('lightbox').classList.add('show');
}

难点三:没配 Token 也能看

静态站最大的体验问题是------新访客不可能先去配 Token 。但 GitHub 对未鉴权请求限制很严:每 IP 每小时仅 60 次,频繁刷新就 403。

解法是只读模式:检测到没有 Token 时,回退到页面所在的部署仓库,用公开 API 读 issues(写操作按钮全部隐藏)。优先级是:

ini 复制代码
?repo=owner/repo  →  localStorage 配置  →  部署仓库兜底
js 复制代码
function getReadRepo() {
  const q = new URLSearchParams(location.search).get('repo');
  if (q?.includes('/')) { /* URL 参数指定 */ }
  if (cfg.owner && cfg.repo) return { owner: cfg.owner, repo: cfg.repo };
  return PAGES_REPO;   // { owner: 'Codeniu', repo: 'prompt-archive' }
}

写按钮按只读状态显隐:

js 复制代码
if (readOnly()) $('addBtn').hidden = true;           // 顶部「新建条目」
$('editToggleBtn').hidden = !isView || ro;            // 详情页「编辑」
${readOnly() ? '' : `<button>销毁</button>`}         // 卡片「销毁」

403 限流时给专门状态页 + 「配置 Token」按钮(鉴权后 5000 次/小时):

js 复制代码
if (isRateLimit && readOnly()) {
  throw new Error('GitHub API 限流(未鉴权每小时仅 60 次)。点 ⚙ 配置 Token 后可提升至 5000 次/小时。');
}

Token 的安全性

很多人怕「Token 写在前端会不会泄露」。这里的关键区分是:

  • 写操作(创建/编辑/删除 issue、上传图片)需要 Token ,但 Token 只存在你自己浏览器 的 localStorage,不会打包进静态文件、不会随请求 URL 暴露(走 Authorization 头)
  • 读操作在只读模式下不需要 Token,用公开 API

也就是说,别人访问你的站,看到的是只读视图,不需要、也拿不到你的 Token。只有你自己点 ⚙ 配置后,才在本机获得写权限。公开仓库用最小权限的 public_repo scope 即可。

设计语言:让它看起来不像默认 SaaS

前端最容易踩的坑是「看起来像教程默认产物」------蓝紫渐变、玻璃拟态、卡片网格 + 中间一张「Most Popular」徽章。这个项目反其道行之,走「编辑档案」风格:

  • 配色 :暖米黄纸张底(oklch(96% 0.014 80))+ 朱砂红强调(oklch(54% 0.180 28)),稀缺使用红色,只在编号、标签、按钮上点睛
  • 字体三联 :Fraunces 衬线扛标题(带 opsz/SOFT 变量轴)、JetBrains Mono 写提示词正文(代码感)、系统 sans 做 UI 控件
  • 卡片 :无阴影、无圆角(--r-sharp: 2px),靠 1px 细线分隔,带 No. 0001 编号像档案条目
  • 弹窗 :直角书写稿纸风格,6px 偏移硬阴影(box-shadow: 6px 6px 0 var(--rule-strong)
  • 动效 :尊重 prefers-reduced-motion,焦点环用 :focus-visible

oklch 而非 hex/rgb 是因为它的感知均匀性更好------调亮度时颜色不会突变脏色。

同一套设计,两种部署形态

项目里其实有两份实现,共用同一套视觉:

形态 文件 后端 数据存储 适合
静态版 docs/index.html GitHub Issues 零成本上线、数据在 GitHub
自托管版 server.py + index.html Python 标准库 本地 prompts.json 内网、不想数据上云

自托管版后端只用标准库(http.server + json),零依赖:

bash 复制代码
python3 server.py
# → Prompt collector running on http://localhost:8000

API 很朴素:

方法 路径 说明
GET /api/prompts 列出全部
POST /api/prompts 新建
PUT /api/prompts/{id} 修订
DELETE /api/prompts/{id} 删除

两套实现刻意保持视觉一致,迁移成本低------本地用自托管版写,想公开就推到 GitHub Pages,数据搬一下即可。

一些不显然的工程细节

1. GitHub Issues API 会把 PR 也混进来。 GET /issues 端点同时返回 issue 和 PR,必须前端过滤:

js 复制代码
return issues.filter(i => !i.pull_request);

2. Issue 的 labels 必须先存在才能附加。 新建 issue 时如果标签没建过会 422,所以要先 POST /labels 逐个建(422 = 已存在,忽略):

js 复制代码
async function ensureLabelsExist(tags) {
  await Promise.allSettled(tags.map(t =>
    fetch(`${ghBase()}/labels`, { method: 'POST', body: JSON.stringify({ name: t, ... }) })
  ));
}

3. 图片上传文件名要唯一。Date.now() + 随机串 避免重名覆盖:

js 复制代码
const path = `images/${Date.now()}-${Math.random().toString(36).slice(2,8)}.${ext}`;

4. 编辑回填要把已有图片带回来。 进入编辑模式时把 issue 现有图片 URL 作为 type: 'existing' 塞回 pending 队列,保存时跳过上传、直接保留:

js 复制代码
pendingImages = images.map(url => ({ type: 'existing', url }));
// 保存时:
if (item.type === 'existing') uploadedUrls.push(item.url);
else uploadedUrls.push(await ghUploadImage(item.name, item.data));

局限与适用边界

诚实地说这套方案的边界:

  • 并发写入:GitHub API 不是为高并发设计的,多人同时编辑会有冲突
  • 限流:未鉴权 60 次/小时,鉴权 5000 次/小时,不适合高频刷新
  • 查询能力弱:只能拉全量再前端筛选,不能像 SQL 那么灵活
  • 单仓库 issue 上限:虽然很大,但不是无限
  • 图片走 raw.githubusercontent.com:偶尔被 CDN 缓存延迟,不适合强实时

适合:个人提示词库、读书笔记、灵感速记、小团队 linklog、FAQ 归档。 不适合:高并发、强一致、复杂关联查询的生产系统。

总结

这个项目想说明的是:很多小工具不需要全栈框架 。当你愿意把「数据库」让给 GitHub Issues、把「后端」让给 Pages 静态托管、把「图片存储」让给 Contents API,你能用一个 HTML 文件换来一个零成本、零运维、数据天然在 GitHub 上可浏览归档的小站。

不是说它比全栈方案「更好」,而是说在轻结构、低并发、个人场景 下,它的成本/收益比非常好看。下次你想做个小工具时,不妨先问自己:这件事,是不是一个 issue 就能装下?


完整代码与部署步骤见 GitHub 仓库,在线 Demo 见 GitHub Pages。MIT 协议,欢迎自取。

相关推荐
摆烂工程师2 小时前
别只拿 GPT-6 Astra 聊天,它真正恐怖的是开始会“干活”了
人工智能·程序员·vibecoding
码哥字节7 小时前
9 个开源 App,治好了我的 vibe coding 焦虑
ai编程·vibecoding
happyfire1 天前
Vibe Coding 一年,我发现 AI 写代码其实是最简单的部分
vibecoding
桦说编程2 天前
【AtomicAgent系列1】变异测试——过去做不起,现在 agent 做得起
后端·ai编程·vibecoding
Behavior7 天前
刚刚,Claude 5.1 发布!全球最强模型来了?
aigc·claude·vibecoding
一用书生7 天前
我给 ChatGPT、DeepSeek、Kimi 都加了一个「保存为笔记」按钮
前端·ai编程·vibecoding
爱丶不疚8 天前
Eval: Agent 说的 Eval 是什么?从单测、TDD 到 Sentry 聊起
前端·ai编程·vibecoding
夏天要喝冰可乐8 天前
从 Idea 到开源插件:我用 Vibe Coding 做了「文章摆渡」
前端·ai编程·vibecoding
Bigger9 天前
别再拿大炮打蚊子了,我给 Codex 加了一个自动驾驶
人工智能·openai·vibecoding