解説一覧

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によるモノレポで、2つのアプリと1つの共有パッケージから成ります。読者向けのNext.jsサイトと、PostgreSQL上のPayload CMSです。Payload CMSは編集者が作業する場所で、サイトもここからコンテンツを取得します。読者は本や学会論文集を閲覧してPDFをダウンロードし、著者はフォームから出版の企画を送ります。すべてNginxの背後にある1台のHetznerサーバーで動き、PM2が両方のアプリをクラスターモードで動かしています。プッシュのたびに、GitHub ActionsでLint、型チェック、テストが走ります。デプロイは手動で開始します。ワークフローはデプロイするコミットそのものでチェックを再実行し、SSHでサーバーに接続して、人が実行するのと同じデプロイスクリプト(インストール、マイグレーション、ビルド、リロード)を走らせます。その後、サイトが応答するまで、外部からヘルスチェック用のエンドポイントをポーリングします。

重要だった判断

  • デプロイは手動でしか始まらない

    プッシュしても何もデプロイされないので、mainに入った問題のあるコミットが勝手に本番環境へ出ることはありません。ワークフローは人が実行するのと同じスクリプトを呼ぶので、手動と自動の手順がずれていくこともありません。

  • 自動ロールバックはしない

    古いコードをチェックアウトしても、すでに実行されたデータベースのマイグレーションは元に戻りません。だから、戻す作業は意図して行う手順のままにしています。デプロイのたびに直前のコミットをサーバーに保存しているので、ロールバックはコマンド1つで済みます。

  • 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