Malton London Press
متاح الآنالموقع الإلكتروني ونظام النشر لدار نشر أكاديمية تعتمد الوصول المفتوح. بنيته، وأدير الخادم، وأنفّذ deploy لكل تغيير.
- دوري
- المطوّر الوحيد
- السنة
- 2026
- التقنيات المستخدمة
- Next.js · Payload CMS · PostgreSQL · Turborepo · PM2 · Nginx · Hetzner · GitHub Actions
مرّر أفقيًا لرؤية المخطط كاملًا.
المشكلة
تحتاج دار النشر الأكاديمية إلى أكثر من موقع إلكتروني. يجب أن يكون كل كتاب وكل ورقة مؤتمر متاحًا للقراءة والتنزيل مجانًا، وأن يتمكن المحررون من النشر دون لمس الكود، وأن يعثر Google Scholar على كل ورقة ويفهرسها. وإن لم يحدث ذلك، يبقى عمل المؤلفين غير مرئي.
كيف بُني
مستودع monorepo مبني بـ pnpm وTurborepo، فيه تطبيقان وحزمة مشتركة: موقع Next.js للقرّاء، وPayload CMS على PostgreSQL، حيث يعمل المحررون ومنه يأخذ الموقع محتواه. يتصفح القرّاء الكتب وأعمال المؤتمرات ويُنزّلون ملفات PDF، ويرسل المؤلفون مقترحات كتبهم عبر نموذج. ويعمل كل شيء على خادم Hetzner واحد خلف Nginx، ويُبقي PM2 التطبيقين في وضع cluster. ومع كل push تعمل في GitHub Actions عمليات lint وفحص الأنواع (type checks) والاختبارات. أما الـ deploy فيبدأ يدويًا: يعيد الـ workflow تشغيل الفحوصات على الـ commit نفسه بالضبط، ويتصل بالخادم عبر SSH، ويشغّل سكربت الـ deploy نفسه الذي قد يشغّله أي شخص (install وmigrate وbuild وreload)، ثم يفحص health endpoint من الخارج مرة بعد مرة حتى يستجيب الموقع.
القرارات المهمة
الـ deploy لا يبدأ إلا يدويًا
لا يحدث أي deploy عند الـ push، لذلك لا يمكن لـ commit سيئ على main أن يصل إلى بيئة الإنتاج من تلقاء نفسه. ويستدعي الـ workflow السكربت نفسه الذي يشغّله الشخص يدويًا، فلا يمكن للمسارين اليدوي والآلي أن يتباعدا.
لا يوجد rollback تلقائي
عمل checkout لكود قديم لا يلغي migration لقاعدة البيانات نُفّذت بالفعل، لذلك تبقى العودة إلى الوراء خطوة مقصودة. ويُحفظ الـ commit السابق على الخادم قبل كل deploy، فيصبح الـ rollback أمرًا واحدًا.
بيانات وصفية يستطيع Google Scholar قراءتها
تحمل صفحة كل ورقة وكل كتاب وسوم استشهاد بصيغة Highwire Press، ويأتي بعد كل مؤلف مؤسسته ومعرّف ORCID الخاص به، حتى يفهرسها Google Scholar وبرامج إدارة المراجع مثل Zotero بشكل صحيح.
كود من المستودع
/** 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
}
السطر 9. يربط Scholar المؤسسة ومعرّف ORCID بوسم المؤلف الذي يسبقهما مباشرة، لذلك لترتيب هذه الوسوم معنى.
السطر 27. تُكتب التواريخ بصيغة YYYY/MM/DD بتوقيت UTC، وهي الصيغة التي يطلبها Scholar.
السطر 31. لا يقبل Scholar ملف PDF إلا إذا كان على الـ host نفسه وفي المجلد نفسه الذي توجد فيه صفحة الورقة، لذلك يُبنى الرابط من مسار الصفحة نفسها.
أين وصل الآن
يعمل منذ يناير 2026 على maltonlondonpress.com. أتولى تشغيله وأنفّذ deploy لكل تغيير.