Loading...
← Всі проекти

Переїзд наукового журналу з uCoz на WordPress

Журнал «Наукові записки СумДПУ. Географічні науки» з 2010 року жив на uCoz, на адресі scinotesgeo.sspu.edu.ua. Я переношу його на WordPress: власна тема, модель даних під статті й випуски та плагін, який за один прохід перетягує весь архів із резервної копії uCoz.

Зараз проєкт працює на локальній копії (XAMPP), до продакшну ще не викочений.

Що саме треба було перенести

Резервна копія uCoz датована 26 квітня 2026 року. Після розбору в ній виявилось 24 сторінки, 2 томи, 16 випусків (2010–2025), 287 статей і 336 файлів, переважно PDF. DOI є лише у 77 статей, старі випуски їх не мали.

Найнеприємніше було не в обсязі. Частина файлів лежала не на основному домені, а на сусідніх uCoz-хостах типу *.at.ua, які деякі антивіруси блокують як шкідливі, тож будь-яке зображення чи PDF звідти на новому сайті підставило б і його, і відвідувача, який просто відкрив сторінку випуску. Тобто посилання на старий сайт були не лише «застарілими», вони могли зламати відвідувачу сторінку.

Чому не стандартний імпортер

Журнальна структура (том → випуск → стаття з авторами, DOI і PDF) у загальні імпортери WordPress не вкладається, тож я розбив задачу на два шари.

Перший: резервна копія (site.txt і loads.txt з uCoz-архіву) перетворюється на один data.json. Там уже нормалізовані сторінки, томи, випуски, статті, список файлів з кандидатами шляхів і карта старих адрес. Другий: плагін «SciNotes Journal — імпорт з uCoz» читає цей JSON і створює все у WordPress.

Такий поділ дав просту річ, якої мені бракувало б у «розумному» імпортері: я можу відкрити JSON, подивитися, що саме піде в базу, і виправити дані до імпорту, а не після.

Імпорт, який не падає на max_execution_time

На локальному XAMPP і, тим паче, на університетському хостингу копіювати 300+ PDF одним запитом — гарантований таймаут. Тому імпорт розбитий на шість кроків, а кожен крок на дрібні AJAX-порції:

php
const SNG_STEPS = array( 'files', 'terms', 'articles', 'pages', 'links', 'finish' );

// sng_step_files(): 8 files per request
if ( microtime( true ) - $started > 20 ) { // big PDFs: stop early, continue next request
	$batch = array_slice( $batch, 0, $i + 1 );
	break;
}

Файли йдуть по вісім, статті по сорок. Якщо великий PDF з'їв 20 секунд, крок зупиняється і продовжує з того ж місця наступним запитом. Стан зберігається в опції sng_import_state, а кожен запис отримує мітку _sng_key, тож повторний запуск оновлює наявне, а не плодить дублікати.

Файли спершу шукаються в локальних папках бекапу (нечутливо до регістру, бо uCoz не надто тримався одного написання). Якщо файлу немає, імпортер може завантажити його зі старого сайту, але ніколи з *.at.ua чи *.ucoz.*. Зображення, яких так і не вдалося знайти, просто прибираються з HTML, а посилання на відсутні файли перетворюються на звичайний текст. Наприкінці лог показує повний список відсутнього, щоб редакція знала, що дозавантажити вручну.

Модель даних: статті як записи, випуски як терміни

Статті стали окремим типом запису sng_article з адресою /statti/…. Випуски — ієрархічна таксономія sng_issue під /arkhiv/…, де верхній рівень це том, а нижній випуск. У випуску в метаданих лежать том, номер, рік, обкладинка й повний PDF; у статті автори, DOI і PDF. Порядок статей у випуску задається через menu_order, як у друкованому змісті.

Спершу модель жила в плагіні. У версії 1.1.0 я переніс її в тему, а плагін лишив суто для одноразового імпорту. Причина прозаїчна: якщо хтось із редакції колись вимкне «непотрібний» плагін, статті не повинні зникнути з сайту.

Пошук по сайту теж довелося підкрутити: стандартний WordPress не шукає по метаполях, а людям потрібно знайти статтю за прізвищем автора.

Старі адреси не мають вести в 404

На uCoz-посилання журналу роками ставили в бібліографіях і базах. Тому імпорт будує карту редиректів: понад сотню старих адрес сторінок, томів і випусків (/index/…/0-27, /publ/…) плюс шлях кожного перенесеного файлу на його нове місце в медіатеці. Усе це 301.

Що не потрапило в карту, але має префікс старих модулів uCoz (/load, /publ, /news, /blog, /index), отримує 302 на архів випусків. Не ідеально, проте краще, ніж порожня сторінка 404.

Google Scholar і дрібниці, які видно лише індексаторам

Для кожної статті тема виводить мета-теги citation_*: назва, автори, журнал, обидва ISSN, рік, том, випуск, DOI, посилання на PDF. Відвідувач їх не бачить, але саме від них залежить, чи коректно Google Scholar підхопить статтю.

Англійська версія без плагінів мультимовності

Окремого англійського сайту в журналу немає, тож я вирішив зробити англійський інтерфейс усередині того самого WordPress: українська на /, англійська на /en/. Без WPML і Polylang.

Мова визначається з шляху ще до того, як WordPress розбирає запит, а фільтр home_url для англійської додає суфікс /en. WordPress сам відрізає його при маршрутизації, а всі згенеровані посилання (пермалінки, меню, форма пошуку) лишаються всередині /en/. Адмінка, REST API та uploads цього не помічають.

Контент перекладається за схемою «один запис, додаткові поля»: у статті sng_title_en, sng_authors_en, sng_abstract_en, у сторінки sng_content_en. Якщо сторінку ще не переклали, англійська версія чесно пише про це і показує український текст з lang="uk". Більшість сторінок саме так зараз і виглядає: переклад контенту — окрема робота для редакції.

Тема

Тема SciNotes Geo написана з нуля: SCSS з токенами в CSS-змінних, шрифти Literata для текстів і Manrope для інтерфейсу, меню з кнопками підменю для клавіатури й тачскрінів. Форма зворотного зв'язку працює без сторонніх плагінів: nonce, honeypot-поле, простенька арифметична капча, ліміт одного повідомлення на хвилину з IP. Повідомлення йдуть поштою і паралельно зберігаються в адмінці окремим типом запису, тож навіть загублений лист не означає загубленого звернення.

Що ще не зроблено

Сайт поки існує лише локально. Попереду перенесення на сервер університету, перевірка редиректів на реальному домені та наповнення англійських полів. Крім того, генерація data.json поки окремий ручний крок, і для наступних випусків вона вже не потрібна: нові статті редакція додаватиме прямо в адмінці.