# Як побудовано цей сайт | NPO EURASIA

> Як побудовано сайт NPO EURASIA: статті беруться з WordPress лише запитами GET і збираються в статичні файли; суворий CSP, оптимізовані зображення, сторінки в Markdown і стрічки.

Colophon

 

# Як працює цей сайт

Статті для цього сайту пишуться у WordPress, а сторінки віддаються як заздалегідь зібрані статичні файли. Для тих, хто цікавиться технологіями, розповідаємо, як зроблено сайт і що ми в ньому захищаємо.

 
Live

 

## Виміряйте цю сторінку на своєму пристрої

Наведені нижче показники записав ваш власний браузер, коли ви відкрили цю сторінку. Вони нікуди не надсилаються, а лише показуються на цьому екрані. Порогові значення відповідають орієнтирам Google для оцінки «Добре» (LCP 2,5 с, CLS 0,1, INP 200 мс).

 
 

- **Час до показу найбільшого елемента (LCP)**：—

- **Зсув макета (CLS)**：—

- **Час від натискання до реакції (INP)**：Натисніть будь-де, щоб виміряти

- **Час до появи першого вмісту (FCP)**：—

- **Час до початку відповіді сервера (TTFB)**：—

- **Обсяг переданого HTML цієї сторінки**：—

- **Як доставлено цю сторінку**：—

 

 

### Порядок надходження файлів

Коли сторінка завантажиться, файли буде показано в порядку надходження.

### Перевірка доставлених файлів

Звіряємо їх із журналом збирання.

 

 

### Технології, які підтримує цей браузер

Перевіряємо у вашому браузері кожну технологію по черзі.

 
- ПеревіркаПлавні переходи між сторінкамиCross-document View Transitions
- ПеревіркаНапрямок переходів (уперед, назад, знизу вгору)View Transition Types
- ПеревіркаПопереднє завантаження наступної сторінкиSpeculation Rules
- ПеревіркаПередавання лише відмінностей починаючи з 2-ї сторінкиCompression Dictionary Transport
- ПеревіркаРух, прив’язаний до прокручуванняScroll-driven Animations
- ПеревіркаВигляд залежно від стану прокручуванняScroll-state Container Queries
- ПеревіркаЗатримка залежно від порядку елементівsibling-index()
- ПеревіркаПружинний рухCSS linear() easing
- ПеревіркаПлавні кутиcorner-shape: squircle
- ПеревіркаПеренесення заголовків за фразамиword-break: auto-phrase
- ПеревіркаПроміжок між японським і латинським текстомtext-autospace
- ПеревіркаАвтоматичне перемикання світлої та темної темиlight-dark()
- ПеревіркаАнімація висоти під час розгортання і згортанняinterpolate-size
- ПеревіркаМеню без скриптівPopover API
- ПеревіркаВизначення напрямку переходуNavigation API
- ПеревіркаБезпечний запис у сторінку зі скриптівTrusted Types

 

- **Спосіб доставки**：Статичні файли (HTML, CSS, зображення)

- **Запити до WordPress**：Лише GET

- **Завантаження вебшрифтів**：0 байтів

- **Вбудовані скрипти та атрибути style**：0

- **Звіти про діяльність на момент збирання**：17 звітів

- **Дата останнього збирання**：23 жовтня 2026 р.

 

 
 

## 01Вміст окремо від оформлення

 
Звіти про діяльність, як і раніше, пишуться в адмінпанелі WordPress. Сайт читає ці записи через публічний REST API WordPress, збирає з них сторінки й віддає їх.

Компонент, що читає дані, може надсилати лише запити GET і не має жодного способу змінити дані WordPress. Попередні результати зберігаються локально, а запити надсилаються з If-None-Match та If-Modified-Since, тож записи, які не змінилися, повторно не завантажуються.

Решта текстів — на головній сторінці, сторінці пожертв тощо — має єдиний оригінал: файл із перевіреними текстами (Design Layer). Сторінки беруть текст звідти, не змінюючи жодного символу. У нас діє правило не публікувати номер телефону організації: наприкінці збирання перевіряються всі створені документи, і якщо в них знайдено номер, збирання завершується помилкою.

 

 
 

## 02Як відбувається збирання

 

- Завантажити тексти й фото з Design Layer, а також записи, категорії та підсумки пожертв із WordPress

- Очистити HTML статей, залишивши лише дозволені елементи та атрибути (скрипти, вбудований вміст і атрибути style видаляються)

- Перетворити фото на AVIF і WebP кількох ширин і додати до назви кожного файлу хеш його вмісту

- Записати HTML кожної сторінки та Markdown із тим самим вмістом

- Записати стрічки, карту сайту, пошуковий індекс, llms.txt і налаштування заголовків відповіді

- Перевірити, чи немає номерів телефону, заздалегідь стиснути Brotli, створити файли відмінностей від словника стиснення й записати SHA-256 усіх файлів

Збирання — це TypeScript, який Node.js виконує напряму. Єдиний зовнішній пакет — sharp для перетворення зображень.

 

 
 

## 03Швидкість відображення

 

- **Зображення**: AVIF і WebP подаються в кількох ширинах через srcset, і браузер вибирає те, що пасує до екрана. Ширину й висоту оголошено заздалегідь, тому макет не зсувається під час завантаження. Поки фото не завантажилося, під ним лежить градієнт із кольорів фото, зменшеного до 2×2, заданий як клас CSS (data: URI не використовуються).

- **Текст**: вебшрифти не завантажуються, текст показується шрифтами, які вже є на вашому пристрої. На шрифти передається 0 байтів.

- **Легше з 2-ї сторінки**: спільні для всіх сторінок частини — шапку, меню, футер тощо — зібрано в один «словник стиснення», який браузер отримує лише раз, коли не зайнятий іншим (Compression Dictionary Transport, RFC 9842). Починаючи з 2-ї сторінки, надсилаються лише відмінності від словника, стиснені Zstandard, тож обсяг HTML приблизно на 40% менший, ніж лише з Brotli, а сторінка пожертв зменшується приблизно до 1/10. Браузерам, що не підтримують словники, як і раніше, надсилається Brotli.

- **Попереднє завантаження наступної сторінки**: Speculation Rules передаються в заголовку HTTP-відповіді, і браузер починає готувати наступну сторінку, наприклад, коли курсор наведено на посилання. Цей спосіб не потребує вбудованого скрипту.

- **Переходи між сторінками**: переходи між сторінками сайту виконуються за допомогою міждокументних View Transitions браузера. Якщо в списку звітів про діяльність натиснути фото запису, воно, змінюючи форму, перетікає у фото на початку запису. Напрямок переходу також залежить від того, куди ви переходите (View Transition Types). Глибші сторінки й наступний запис з’являються справа; коли ви повертаєтеся на рівень вище кнопкою браузера «Назад» або через навігаційний ланцюжок, сторінка з’являється зліва, а сторінка пожертв висувається знизу екрана. Сторінка, яку залишають, записує свій рівень, а сторінка, що відкривається, визначає напрямок за своїм рівнем і позицією в історії Navigation API. Шапка і кнопка пожертви внизу екрана залишаються на місці, змінюється лише сторінка під ними.

- **Вимірювання на реальних відвідуваннях**: на робочому сайті час до показу сторінки (LCP), зсув макета (CLS) і час від натискання до реакції (INP) рахуються за значеннями, які записали браузери відвідувачів. До Google Analytics надсилається лише оцінка кожного показника («Добре», «Потрібно покращити» або «Погано») та його значення. Мета — перевірити швидкість на справжніх пристроях і з’єднаннях, а не в тестовому середовищі.

- **CSS і JavaScript**: стилі зібрано в 1 таблицю, порядок яких закріплено каскадними шарами. Скрипти завантажуються як модулі — лише ті, що потрібні цій сторінці.

Speculation-Rules: "/speculation.json"
 

 
 

## 04Шлях до Києва

 
На мапі на головній сторінці SVG маршруту від порту Мідзусіма до Києва має атрибут pathLength="10000". Довжину маршруту прийнято за 10 000, тож 1 одиниця на маршруті відповідає рівно 1 км.

Положення машини швидкої допомоги, жовту лінію пройденого шляху та синю лінію, що з’являється, коли ви змінюєте суму пожертви, обчислює CSS із типізованих користувацьких властивостей (--km-now і --you). JavaScript лише змінює --you відповідно до введеної суми й не обчислює координат машини.

.map .vehicle {
offset-path: var(--route);
offset-distance: calc(
min(10000, (var(--km-now) + var(--you)) * var(--drive)) / 100 * 1%
);
}
Коли мапа з’являється на екрані, машина швидкої допомоги їде від порту до сьогоднішньої точки. Це анімація, прив’язана до прокручування (animation-timeline: view()), і її теж намальовано лише засобами CSS.

 

 
 

## 05Рух і налаштування, що його зменшують

 
Поява розділів і повільне збільшення фото — це CSS-анімації, прив’язані до позиції прокручування. Якщо на вашому пристрої увімкнено «Зменшення руху» чи подібне налаштування, усе показується без руху. Переходи між сторінками за цього налаштування теж вимикаються.

Кнопки та картки рухаються як на пружині. Приблизно 30 точок узято з рівняння загасних коливань і записано в CSS-функцію linear(): під час натискання елемент одразу просідає, а коли відпустити — трохи проскакує і зупиняється. Скрипти не використовуються.

 --spring: linear(0, 0.034, 0.118, 0.231, … 1.046, … 1);
.btn { transition: transform .45s var(--spring); }
 

 
 

## 06Правила безпеки

 

- **Content Security Policy**: скрипти й стилі завантажуються лише з файлів цього сайту. Немає жодного вбудованого скрипту, атрибута style чи елемента <style>. Навіть динамічні значення передаються через класи CSS і користувацькі властивості.

- **Trusted Types**: увімкнено require-trusted-types-for 'script', і не дозволено жодної політики. Скрипти не можуть записувати HTML через innerHTML, а навіть результати пошуку на сайті збираються з DOM-елементів по одному.

- **Subresource Integrity**: файли CSS і JavaScript мають атрибут integrity із SHA-384 свого вмісту. Файл, змінений під час доставки, браузер не виконає.

- **Інші заголовки відповіді**: Cross-Origin-Opener-Policy і Cross-Origin-Resource-Policy обмежують взаємодію з іншими сайтами, а Permissions-Policy забороняє використання камери, мікрофона, геолокації тощо.

- **Текст статей**: HTML, отриманий із WordPress, публікується лише після того, як у ньому залишено тільки дозволені елементи, атрибути та форми URL (http, https, mailto і шляхи в межах сайту).

 

 
 

## 07Типографіка

 
Проміжки між японським і латинським текстом (text-autospace), стискання пробілів біля розділових знаків (text-spacing-trim) і перенесення заголовків за фразами (word-break: auto-phrase) покладено на можливості браузера. Браузери без їхньої підтримки показують текст зі звичайною версткою.

 

 
 

## 08Машинозчитувані формати

 
Додайте index.md у кінець адреси будь-якої сторінки — і прочитаєте той самий вміст у Markdown (наприклад: [ця сторінка в Markdown](https://npoeurasia.org/uk/colophon/index.md)). Сторінки також містять структуровані дані (JSON-LD).

- [/feed.xmlЗвіти про діяльність у форматі RSS 2.0 (японською)](https://npoeurasia.org/feed.xml)

- [/feed.jsonЗвіти про діяльність у форматі JSON Feed 1.1 (японською)](https://npoeurasia.org/feed.json)

- [/llms.txtПокажчик Markdown-версій усіх сторінок (японською)](https://npoeurasia.org/llms.txt)

- [/sitemap.xmlКарта сайту](https://npoeurasia.org/sitemap.xml)

- [/search-index.jsonІндекс для пошуку на сайті. Пошук відбувається у вашому браузері](https://npoeurasia.org/search-index.json)

- [/.well-known/build.jsonЖурнал збирання: дата й час збирання, а також розмір і SHA-256 кожного опублікованого файлу](https://npoeurasia.org/.well-known/build.json)

 

 
 

## 09Навіть без JavaScript

 
Основний текст, кнопки пожертв, меню та розгортання відповідей у поширених запитаннях працюють і з вимкненим JavaScript. Меню зроблено на HTML-атрибуті popover, а розгортання запитань — на елементі <details>. JavaScript використовується лише там, де він додає зручності, як-от керування сумою на мапі чи пошук на сайті.
