Alle Artikel

Malton London Press

Online

Die Website und das Publikationssystem eines wissenschaftlichen Open-Access-Verlags. Ich habe beides gebaut, betreibe den Server und deploye jede Änderung.

Meine Rolle
Alleiniger Entwickler
Jahr
2026
Gebaut mit
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

Scroll seitlich, um das ganze Diagramm zu sehen.

ArchitekturPUSH → CHECKS → DEPLOY BY HAND → HEALTH CHECK

Das Problem

Ein Wissenschaftsverlag braucht mehr als eine Website. Jedes Buch und jeder Konferenzbeitrag muss kostenlos lesbar und herunterladbar sein, die Redaktion muss veröffentlichen können, ohne Code anzufassen, und Google Scholar muss jeden Beitrag finden und indexieren. Sonst bleibt die Arbeit der Autoren unsichtbar.

Wie es gebaut ist

Ein Monorepo mit pnpm und Turborepo, zwei Apps und einem gemeinsamen Paket: eine Next.js-Website für die Leser und Payload CMS auf PostgreSQL, wo die Redaktion arbeitet und woher die Website ihre Inhalte bezieht. Leser stöbern in Büchern und Tagungsbänden und laden die PDFs herunter, Autoren schicken Buchvorschläge über ein Formular. Alles läuft auf einem einzigen Hetzner-Server hinter Nginx, und PM2 betreibt beide Apps im Cluster-Modus. Bei jedem Push laufen Linting, Type-Checks und Tests in GitHub Actions. Ein Deploy wird von Hand gestartet: Der Workflow führt die Checks noch einmal auf genau diesem Commit aus, verbindet sich per SSH mit dem Server, startet dasselbe Deploy-Skript, das auch ein Mensch ausführen würde (install, migrate, build, reload), und fragt dann von außen einen Health-Endpoint ab, bis die Website antwortet.

Entscheidungen, auf die es ankam

  • Deploys starten nur von Hand

    Bei einem Push wird nichts deployt, also kann ein fehlerhafter Commit auf main nicht von selbst in Produktion landen. Der Workflow ruft dasselbe Skript auf, das auch ein Mensch ausführen würde, also können der manuelle und der automatisierte Weg nicht auseinanderdriften.

  • Kein automatischer Rollback

    Das Auschecken von altem Code macht eine bereits gelaufene Datenbankmigration nicht rückgängig, deshalb bleibt der Weg zurück ein bewusster Schritt. Vor jedem Deploy wird der vorherige Commit auf dem Server gespeichert, damit ist der Rollback ein einziger Befehl.

  • Metadaten, die Google Scholar lesen kann

    Jede Beitrags- und Buchseite enthält Citation-Tags im Format von Highwire Press, bei denen auf jeden Autor die jeweils eigene Institution und ORCID folgen, damit Google Scholar und Literaturverwaltungen wie Zotero sie korrekt indexieren.

Code aus dem Repo

apps/web/src/lib/citation.tsDie Citation-Tags eines Konferenzbeitrags für 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. Zeile 9. Scholar ordnet eine Institution und eine ORCID dem Autor-Tag zu, der direkt davor steht, deshalb hat die Reihenfolge dieser Tags eine Bedeutung.

  2. Zeile 27. Datumsangaben gehen als YYYY/MM/DD in UTC raus, in der Form, die Scholar verlangt.

  3. Zeile 31. Scholar akzeptiert ein PDF nur vom selben Host und aus demselben Verzeichnis wie die Seite des Beitrags, deshalb wird der Link aus dem Pfad der Seite selbst gebaut.

Wo es heute steht

Seit Januar 2026 online unter maltonlondonpress.com. Ich halte die Website am Laufen und deploye jede Änderung.

Nächster ArtikelResearchGo