Todos os artigos

Malton London Press

No ar

O site e o sistema de publicação de uma editora acadêmica de acesso aberto. Eu o desenvolvi, cuido do servidor e faço o deploy de cada mudança.

Meu papel
Único desenvolvedor
Ano
2026
Feito com
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

Role para o lado para ver o diagrama inteiro.

ArquiteturaPUSH → CHECKS → DEPLOY BY HAND → HEALTH CHECK

O problema

Uma editora acadêmica precisa de mais do que um site. Cada livro e cada artigo de congresso precisa poder ser lido e baixado de graça, os editores precisam publicar sem mexer em código e o Google Scholar precisa encontrar e indexar cada artigo. Se isso não acontece, o trabalho dos autores fica invisível.

Como foi construído

Um monorepo com pnpm e Turborepo, com dois apps e um pacote compartilhado: um site em Next.js para os leitores e o Payload CMS sobre PostgreSQL, onde os editores trabalham e de onde o site tira o conteúdo. Os leitores navegam por livros e anais de congressos e baixam os PDFs, e os autores enviam propostas de livro por um formulário. Tudo roda em um único servidor da Hetzner atrás do Nginx, e o PM2 mantém os dois apps em modo cluster. Cada push roda lint, checagem de tipos e testes no GitHub Actions. O deploy é disparado manualmente: o workflow roda as verificações de novo no commit exato, conecta-se ao servidor por SSH, executa o mesmo script de deploy que uma pessoa executaria (install, migrate, build, reload) e depois faz polling de um endpoint de health de fora até o site responder.

Decisões importantes

  • O deploy só é disparado manualmente

    Um push não dispara deploy nenhum, então um commit ruim na main não chega à produção sozinho. O workflow chama o mesmo script que uma pessoa rodaria, por isso o caminho manual e o automatizado não têm como divergir.

  • Sem rollback automático

    Fazer checkout de um código antigo não desfaz uma migration de banco de dados que já rodou, então voltar atrás continua sendo um passo deliberado. O commit anterior é salvo no servidor antes de cada deploy, o que faz do rollback um único comando.

  • Metadados que o Google Scholar consegue ler

    Cada página de artigo e de livro traz as tags de citação do Highwire Press, com cada autor seguido da própria instituição e do próprio ORCID, para que o Google Scholar e gerenciadores de referências como o Zotero indexem essas páginas corretamente.

Código do repositório

apps/web/src/lib/citation.tsTags de citação do Google Scholar para um artigo de congresso
/** 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. Linha 9. O Scholar associa uma instituição e um ORCID à tag de autor que vem logo antes, então a ordem dessas tags tem significado.

  2. Linha 27. As datas saem como YYYY/MM/DD em UTC, o formato que o Scholar pede.

  3. Linha 31. O Scholar só aceita um PDF que esteja no mesmo host e diretório da página do artigo, então o link é montado a partir do caminho da própria página.

Como está hoje

No ar desde janeiro de 2026 em maltonlondonpress.com. Eu cuido para que continue no ar e faço o deploy de cada mudança.

Próximo artigoResearchGo