用 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 协议,欢迎自取。

相关推荐
哈基咪怎么可能是AI10 小时前
dsh插件开发踩坑实录(写给正在build dsh插件的ai agent)
deepseek·vibecoding
vibecoding日记11 小时前
dsh插件开发踩坑实录(写给正在build dsh插件的ai agent)
编程工具·ai助手·vibecoding
勤劳打代码16 小时前
从 Demo 搭建的Flutter 演示项目 —— Forge
flutter·客户端·vibecoding
潘锦2 天前
使用 Vibe Coding 的这6 种后遗症,你有吗?
cto·vibecoding
kisshyshy2 天前
《从屎山到秩序:Vibe Coding 95 驾驭术全公开》
人工智能·代码规范·vibecoding
无责任此方_修行中2 天前
插件+1:MiaoMint —— 类 RayCast 的标签管理工具
前端·javascript·vibecoding
白雾茫茫丶2 天前
VibeCoding 一套 Admin 系统,五种技术栈实现
前端·ai编程·vibecoding
ClouGence2 天前
你 Vibe Coding 完的网站,不会直接上线了吧?
ai编程·测试·vibecoding
夏天要喝冰可乐9 天前
一处写作,多平台分发:我给掘金做了一款开源 Chrome 插件
前端·chrome·vibecoding