全部项目详解

Malton London Press

已上线

一家开放获取学术出版社的网站和出版系统。我负责开发、运维服务器,并部署每一次改动。

我的角色
独立开发
年份
2026
使用技术
Next.js · Payload CMS · PostgreSQL · Turborepo · PM2 · Nginx · Hetzner · GitHub Actions
GITHUB ACTIONS: LINT, TYPES, TESTSDEPLOY SCRIPT: INSTALL, MIGRATE, BUILDNGINX ON HETZNERNEXT.JS: BOOKS, PAPERS, PDFSPAYLOAD CMS: EDITORSPOSTGRESQLBY HANDPM2 RELOADREADERSCONTENT APIPREVIOUS COMMIT SAVED FOR ROLLBACKGOOGLE SCHOLAR CITATION TAGS

横向滚动可以看到完整的图。

架构PUSH → CHECKS → DEPLOY BY HAND → HEALTH CHECK

问题

学术出版社需要的不只是一个网站。每本书、每篇会议论文都必须能免费阅读和下载,编辑要能不碰代码就发布内容,Google Scholar 也要能找到并收录每篇论文。否则,作者的成果就没人看得到。

实现方式

一个基于 pnpm 和 Turborepo 的 monorepo,包含两个应用和一个共享包:面向读者的 Next.js 站点,以及跑在 PostgreSQL 上的 Payload CMS,编辑在 CMS 里工作,站点的内容也从这里来。读者可以浏览图书和会议论文集、下载 PDF,作者通过表单提交出书提案。所有服务都跑在一台 Hetzner 服务器上,前面是 Nginx,PM2 让两个应用都以集群模式运行。每次 push 都会在 GitHub Actions 里跑 lint、类型检查和测试。部署要手动触发:工作流会在要部署的那个提交上重新跑一遍检查,通过 SSH 连上服务器,执行人工部署时用的同一个部署脚本(安装、迁移、构建、重载),然后从外部轮询健康检查接口,直到站点有响应。

关键决策

  • 部署只能手动触发

    push 不会触发任何部署,所以 main 分支上一个有问题的提交不会自己跑到生产环境。工作流调用的是人工执行的同一个脚本,所以手动和自动两条路径不会逐渐分叉。

  • 不做自动回滚

    检出旧代码并不能撤销已经执行过的数据库迁移,所以回退始终是一个需要刻意执行的步骤。每次部署前,服务器上都会保存上一个提交,这样回滚只需要一条命令。

  • Google Scholar 能读懂的元数据

    每篇论文和每本书的页面都带有 Highwire Press 引文标签,每位作者后面紧跟着该作者自己的机构和 ORCID,这样 Google Scholar 和 Zotero 这类文献管理工具就能正确索引。

仓库里的代码

apps/web/src/lib/citation.ts会议论文的 Google Scholar 引文标签
/** One citation_author per person, each followed by its own institution and ORCID. */
function authorTags(people: Person[] | null | undefined): CitationTag[] {
  const tags: CitationTag[] = []
  for (const person of people ?? []) {
    const name = person.name?.trim()
    if (!name) continue
    tags.push(['citation_author', name])
    const institution = person.affiliation?.trim()
    if (institution) tags.push(['citation_author_institution', institution])1
    const orcid = orcidUrl(person.orcid)
    if (orcid) tags.push(['citation_author_orcid', orcid])
  }
  return tags
}
 
// …
 
/** Tags for a conference paper page (/proceedings/[volume]/[paper]). */
export function paperCitationTags(
  paper: Pick<Paper, 'title' | 'slug' | 'authors' | 'doi' | 'pdf' | 'pagesRange'>,
  volume: Pick<Book, 'title' | 'slug' | 'publicationDate' | 'isbn' | 'conference'>,
  siteUrl: string = publicSiteUrl(),
): CitationTag[] {
  const tags: CitationTag[] = []
  add(tags, 'citation_title', paper.title)
  tags.push(...authorTags(paper.authors))
  add(tags, 'citation_publication_date', citationDate(volume.publicationDate))2
  // …
  const site = siteUrl.replace(/\/+$/, '')
  // Same host and directory as this page (Scholar ignores a PDF anywhere else).
  if (hasFile(paper.pdf)) add(tags, 'citation_pdf_url', `${site}${paperPdfPath(volume.slug, paper.slug)}`)3
  add(tags, 'citation_abstract_html_url', `${site}/proceedings/${volume.slug}/${paper.slug}`)
  add(tags, 'citation_language', 'en')
  return tags
}
  1. 第 9 行. Scholar 会把机构和 ORCID 关联到紧挨在它们前面的作者标签上,所以这些标签的顺序是有意义的。

  2. 第 27 行. 日期按 Scholar 要求的格式输出,即 UTC 时间的 YYYY/MM/DD。

  3. 第 31 行. Scholar 只接受与论文页面同一主机、同一目录下的 PDF,所以链接是根据页面自身的路径拼出来的。

目前进展

自 2026 年 1 月起在 maltonlondonpress.com 上线。网站由我负责运维,每一次改动也都由我部署。

下一篇ResearchGo