Tous les articles

Malton London Press

En ligne

Le site web et le système de publication d'un éditeur académique en libre accès. Je l'ai développé, je gère le serveur et je déploie chaque modification.

Mon rôle
Seul développeur
Année
2026
Développé avec
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

Faites défiler horizontalement pour voir tout le schéma.

ArchitecturePUSH → CHECKS → DEPLOY BY HAND → HEALTH CHECK

Le problème

Un éditeur académique a besoin de plus qu'un site web. Chaque livre et chaque article de conférence doit pouvoir être lu et téléchargé gratuitement, l'équipe éditoriale doit pouvoir publier sans toucher au code, et Google Scholar doit trouver et indexer chaque article. Sinon, le travail des auteurs reste invisible.

Comment c'est construit

Un monorepo pnpm et Turborepo avec deux applications et un package partagé : un site Next.js pour les lecteurs, et Payload CMS sur PostgreSQL, où travaille l'équipe éditoriale et d'où le site tire son contenu. Les lecteurs parcourent les livres et les actes de conférence et téléchargent les PDF, et les auteurs envoient leurs propositions de livres via un formulaire. Tout tourne sur un seul serveur Hetzner derrière Nginx, et PM2 maintient les deux applications en mode cluster. Chaque push lance le lint, la vérification des types et les tests dans GitHub Actions. Un déploiement se lance à la main : le workflow relance les vérifications sur le commit exact, se connecte au serveur en SSH, exécute le même script de déploiement qu'une personne lancerait (install, migrate, build, reload), puis interroge depuis l'extérieur un endpoint de health check jusqu'à ce que le site réponde.

Les décisions qui ont compté

  • Les déploiements ne se lancent qu'à la main

    Un push ne déclenche aucun déploiement, donc un mauvais commit sur main ne peut pas partir en production tout seul. Le workflow appelle le même script qu'une personne lancerait, donc le chemin manuel et le chemin automatisé ne peuvent pas diverger.

  • Pas de rollback automatique

    Un checkout de l'ancien code n'annule pas une migration de base de données déjà exécutée, donc revenir en arrière reste une action délibérée. Le commit précédent est enregistré sur le serveur avant chaque déploiement, ce qui fait du rollback une seule commande.

  • Des métadonnées lisibles par Google Scholar

    Chaque page d'article et de livre porte des balises de citation au format Highwire Press, où chaque auteur est suivi de sa propre institution et de son ORCID, pour que Google Scholar et les gestionnaires de références comme Zotero les indexent correctement.

Code tiré du dépôt

apps/web/src/lib/citation.tsLes balises de citation Google Scholar d'un article de conférence
/** 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. Ligne 9. Scholar rattache une institution et un ORCID à la balise d'auteur qui les précède immédiatement, donc l'ordre de ces balises a un sens.

  2. Ligne 27. Les dates sortent au format YYYY/MM/DD en UTC, la forme que demande Scholar.

  3. Ligne 31. Scholar n'accepte un PDF que s'il vient du même hôte et du même répertoire que la page de l'article, donc le lien est construit à partir du chemin de la page elle-même.

Où en est le projet

En ligne depuis janvier 2026 sur maltonlondonpress.com. Je m'occupe de le faire tourner et je déploie chaque modification.

Article suivantResearchGo