feat: translate all pages, UI messages and docs and fix SEO redirects and indexing

This commit is contained in:
alam00000 committed 2026-10-25 12:55:21 +05:30
1 parent 5e019de5ae
commit 4edb1718cc
3344 files changed
+415643 -4759

No files matched your search

+275
View File
@@ -0,0 +1,275 @@
# Розгортання з Apache
Розмістіть BentoPDF за допомогою Apache HTTP Server.
## Вимоги
- Apache 2.4+
- Увімкнений mod_rewrite
- SSL-сертифікат (рекомендовано)
## Крок 1: зберіть проєкт
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
Щоб налаштувати брендинг, задайте змінні середовища перед збіркою:
```bash
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
```
## Крок 2: скопіюйте файли
```bash
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## Крок 3: конфігурація Apache
Створіть `/etc/apache2/sites-available/bentopdf.conf`:
```apache
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
DocumentRoot /var/www/bentopdf
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
<Directory /var/www/bentopdf>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# WASM MIME type
AddType application/wasm .wasm
# Prevent double-compression of pre-compressed files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json application/wasm
</IfModule>
# Cache headers
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
# Required headers for SharedArrayBuffer (LibreOffice WASM)
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "cross-origin"
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
# Pre-compressed LibreOffice WASM files
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
</VirtualHost>
```
## Крок 4: .htaccess для маршрутизації
Створіть `/var/www/bentopdf/.htaccess`:
```apache
RewriteEngine On
RewriteBase /
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# ADD .HTML EXTENSION (ROOT LEVEL)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /404.html
```
## Крок 5: увімкніть потрібні модулі
```bash
sudo a2enmod rewrite
sudo a2enmod ssl
sudo a2enmod headers
sudo a2enmod expires
sudo a2enmod deflate
```
## Крок 6: увімкніть сайт
```bash
sudo a2ensite bentopdf.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
```
## Розгортання в підкаталозі
Щоб розмістити за адресою `/pdf/`:
1. Зберіть із базовим URL:
```bash
BASE_URL=/pdf/ npm run build
```
2. Оновіть `.htaccess`:
```apache
RewriteEngine On
RewriteBase /pdf/
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Language routes
RewriteRule ^en/?$ /pdf/ [R=301,L]
RewriteRule ^en/(.+)$ /pdf/$1 [R=301,L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf/$1/index.html [L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf/404.html
```
## Усунення несправностей
### Помилки 404 для WASM
Переконайтеся, що налаштовано MIME-тип:
```apache
AddType application/wasm .wasm
```
### Sign PDF або Form Filler показує порожній переглядач (помилка MIME для `.mjs`)
Якщо в консолі браузера з'являється:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
…і запит, що не вдався, закінчується на `.mjs`, у конфігурації Apache бракує зіставлення `.mjs → application/javascript`. Вбудований PDF-переглядач постачає файли ES-модулів з розширенням `.mjs`, а стандартний `mime.types` в Apache їх за замовчуванням не містить.
Додайте до VirtualHost або `.htaccess`:
```apache
AddType application/javascript .mjs
```
Потім перезавантажте Apache:
```bash
sudo apache2ctl configtest && sudo systemctl reload apache2
```
Щоб перевірити: у DevTools браузера → вкладка Network знайдіть невдалий запит `.mjs` і перегляньте заголовок відповіді `Content-Type`. Тепер він має бути `application/javascript`, а не `application/octet-stream`.
### Rewrite не працює
Перевірте, чи увімкнено mod_rewrite:
```bash
sudo a2enmod rewrite
```
### Конвертація Word/ODT/Excel у PDF не працює
LibreOffice WASM потребує `SharedArrayBuffer`, для чого потрібні такі заголовки:
```apache
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
```
Також потрібен безпечний контекст. `http://localhost` підходить для локального тестування, але `http://192.168.x.x` та інші LAN-адреси зазвичай вимагають HTTPS. Якщо заголовки є, але `window.crossOriginIsolated` усе ще `false`, перевірте, чи сторінка не відкривається через звичайний HTTP з origin, що не є loopback.
Попередньо стиснуті файли `.wasm.gz` і `.data.gz` також потребують правильного `Content-Encoding`:
```apache
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
```
Переконайтеся, що `mod_headers` увімкнено: `sudo a2enmod headers`
### Доступ заборонено
```bash
sudo chown -R www-data:www-data /var/www/bentopdf
sudo chmod -R 755 /var/www/bentopdf
```
+200
View File
@@ -0,0 +1,200 @@
# Розгортання на AWS S3 + CloudFront
Розмістіть BentoPDF на AWS для максимального контролю та масштабованості.
## Архітектура
```
User → CloudFront (CDN) → S3 (Static Files)
```
## Крок 1: створіть бакет S3
```bash
# Create bucket
aws s3 mb s3://your-bentopdf-bucket --region us-east-1
# Enable static website hosting
aws s3 website s3://your-bentopdf-bucket \
--index-document index.html \
--error-document index.html
```
## Крок 2: зберіть і завантажте
```bash
# Build the project (optionally with custom branding)
# VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
npm run build
# Sync to S3
aws s3 sync dist/ s3://your-bentopdf-bucket \
--delete \
--cache-control "max-age=31536000"
# Set correct MIME types for WASM
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.wasm" \
--content-type "application/wasm" \
--metadata-directive REPLACE
# Set correct MIME type for ES module files (.mjs)
# S3 defaults .mjs to application/octet-stream, which browsers refuse to
# execute as ES modules. The bundled PDF viewer (Sign PDF, Form Filler)
# ships ES modules with .mjs extensions and will fail without this step.
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.mjs" \
--content-type "application/javascript" \
--metadata-directive REPLACE
```
## Крок 3: створіть дистрибуцію CloudFront
```bash
aws cloudfront create-distribution \
--origin-domain-name your-bentopdf-bucket.s3.amazonaws.com \
--default-root-object index.html
```
Або скористайтеся консоллю AWS:
1. Перейдіть до CloudFront → Create distribution
2. Origin domain: виберіть свій бакет S3
3. Увімкніть "Origin Access Control"
4. Default root object: `index.html`
5. Створіть дистрибуцію
## Крок 3b: політика заголовків відповіді (обов'язково для LibreOffice WASM)
Конвертація на основі LibreOffice (Word, Excel, PowerPoint у PDF) потребує `SharedArrayBuffer`, для чого потрібні певні заголовки відповіді. Створіть CloudFront Response Headers Policy:
1. Перейдіть до CloudFront → Policies → Response headers
2. Створіть власну політику з такими заголовками:
| Заголовок | Значення |
| ------------------------------ | -------------- |
| `Cross-Origin-Embedder-Policy` | `require-corp` |
| `Cross-Origin-Opener-Policy` | `same-origin` |
| `Cross-Origin-Resource-Policy` | `cross-origin` |
3. Прикріпіть політику до стандартної поведінки кешу (default cache behavior) вашої дистрибуції
Або через CLI:
```bash
aws cloudfront create-response-headers-policy \
--response-headers-policy-config '{
"Name": "BentoPDF-COEP-COOP",
"CustomHeadersConfig": {
"Quantity": 3,
"Items": [
{"Header": "Cross-Origin-Embedder-Policy", "Value": "require-corp", "Override": true},
{"Header": "Cross-Origin-Opener-Policy", "Value": "same-origin", "Override": true},
{"Header": "Cross-Origin-Resource-Policy", "Value": "cross-origin", "Override": true}
]
}
}'
```
## Крок 3c: метадані S3 для попередньо стиснутих файлів WASM
Файли LibreOffice WASM попередньо стиснуті (`.wasm.gz`, `.data.gz`). Задайте правильні Content-Type і Content-Encoding, щоб браузери розпаковували їх:
```bash
# Set correct headers for soffice.wasm.gz
aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \
s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \
--content-type "application/wasm" \
--content-encoding "gzip" \
--metadata-directive REPLACE
# Set correct headers for soffice.data.gz
aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \
s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \
--content-type "application/octet-stream" \
--content-encoding "gzip" \
--metadata-directive REPLACE
```
::: warning Важливо
Без політики заголовків відповіді `SharedArrayBuffer` недоступний, і конвертація LibreOffice WASM зависатиме приблизно на 55%. Без правильного Content-Encoding для файлів `.gz` браузер отримує необроблені байти gzip, і компіляція WASM завершується помилкою.
:::
## Крок 4: політика бакета S3
Дозвольте CloudFront доступ до бакета:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bentopdf-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
```
## Крок 5: власні сторінки помилок
Налаштуйте повернення `index.html` для 404 для маршрутизації SPA:
1. CloudFront → Error pages
2. Створіть власну відповідь на помилку:
- HTTP error code: 404
- Response page path: `/index.html`
- HTTP response code: 200
## Оцінка вартості
| Ресурс | Орієнтовна вартість |
| ------------------------- | ------------------- |
| Сховище S3 (~500MB) | ~$0.01/міс. |
| CloudFront (1TB трафіку) | ~$85/міс. |
| CloudFront (10GB трафіку) | ~$0.85/міс. |
::: tip
Використовуйте S3 Intelligent Tiering, щоб оптимізувати витрати на файли, до яких звертаються нечасто.
:::
## Автоматизація за допомогою Terraform
```hcl
# main.tf
resource "aws_s3_bucket" "bentopdf" {
bucket = "your-bentopdf-bucket"
}
resource "aws_cloudfront_distribution" "bentopdf" {
origin {
domain_name = aws_s3_bucket.bentopdf.bucket_regional_domain_name
origin_id = "S3Origin"
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
}
}
```
+154
View File
@@ -0,0 +1,154 @@
# Розгортання на Cloudflare Pages
[Cloudflare Pages](https://pages.cloudflare.com) пропонує швидкий глобальний хостинг статичних сайтів з необмеженим трафіком.
## Швидке розгортання
1. Перейдіть до [Cloudflare Pages](https://dash.cloudflare.com/?to=/:account/pages)
2. Натисніть "Create a project"
3. Підключіть свій репозиторій GitHub
## Конфігурація збірки
| Параметр | Значення |
| ---------------------- | --------------- |
| Framework preset | None |
| Build command | `npm run build` |
| Build output directory | `dist` |
| Root directory | `/` |
## Змінні середовища
Додайте їх у Settings → Environment variables:
| Змінна | Значення |
| ----------------------- | ---------------------------------------------------- |
| `NODE_VERSION` | `18` |
| `SIMPLE_MODE` | `false` (необов'язково) |
| `VITE_BRAND_NAME` | Власна назва бренду (необов'язково) |
| `VITE_BRAND_LOGO` | Шлях до логотипа відносно `public/` (необов'язково) |
| `VITE_FOOTER_TEXT` | Власний текст футера/авторських прав (необов'язково) |
| `VITE_DEFAULT_LANGUAGE` | Типова мова інтерфейсу, напр. `fr` (необов'язково) |
## Файл конфігурації
Створіть `_headers` у папці `public`:
```
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: cross-origin
# Pre-compressed LibreOffice WASM binary
/libreoffice-wasm/soffice.wasm.gz
Content-Type: application/wasm
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
# Pre-compressed LibreOffice WASM data
/libreoffice-wasm/soffice.data.gz
Content-Type: application/octet-stream
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
# Cache WASM files aggressively
/*.wasm
Cache-Control: public, max-age=31536000, immutable
Content-Type: application/wasm
# Service worker
/sw.js
Cache-Control: no-cache
```
::: warning Важливо
Заголовки `Cross-Origin-Embedder-Policy` і `Cross-Origin-Opener-Policy` обов'язкові для конвертації Word/ODT/Excel/PowerPoint у PDF. Без них `SharedArrayBuffer` недоступний, і рушій LibreOffice WASM не зможе ініціалізуватися.
:::
Створіть `_redirects` для маршрутизації SPA:
```
/* /index.html 200
```
## Власний домен
1. Перейдіть до свого проєкту Pages
2. Натисніть "Custom domains"
3. Додайте свій домен
4. Cloudflare автоматично налаштує DNS, якщо домен розміщено на Cloudflare
## Переваги
- **Безкоштовний необмежений трафік**
- **Глобальна CDN** з понад 300 вузлами
- **Автоматичний HTTPS**
- **Попередні розгортання** для pull request
- **Швидкі збірки**
## Усунення несправностей
### Завантаження великих файлів
Cloudflare Pages підтримує файли до 25 MB. Модулі WASM мають вміститися, але якщо ви впираєтеся в ліміти, розгляньте:
```bash
# Split large files during build
npm run build
```
### Ліміти розміру Worker
Якщо ви використовуєте Cloudflare Workers для складної маршрутизації, зверніть увагу на ліміт 1 MB для безкоштовних планів.
## Worker CORS-проксі (для цифрових підписів)
Інструмент цифрового підпису потребує CORS-проксі для отримання ланцюжків сертифікатів. Розгорніть вбудований worker:
```bash
cd cloudflare
npx wrangler login
npx wrangler deploy
```
### Функції безпеки
| Функція | Опис |
| --------------------------- | ------------------------------- |
| **Обмеження URL** | Дозволено лише URL сертифікатів |
| **Ліміт розміру файлу** | Макс. 10MB на запит |
| **Обмеження частоти** | 60 запитів/IP/хв (потрібен KV) |
| **Блокування приватних IP** | Блокує localhost і внутрішні IP |
### Увімкнення обмеження частоти
```bash
# Create KV namespace
npx wrangler kv namespace create "RATE_LIMIT_KV"
# Add to wrangler.toml with returned ID:
# [[kv_namespaces]]
# binding = "RATE_LIMIT_KV"
# id = "YOUR_ID"
npx wrangler deploy
```
### Збірка з URL проксі
```bash
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
```
Або з Docker:
```bash
export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf .
```
> **Примітка:** Налаштування підпису HMAC дивіться в [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required).
+180
View File
@@ -0,0 +1,180 @@
# CORS-проксі для отримання сертифікатів
Інструмент цифрового підпису використовує CORS-проксі, щоб отримувати сертифікати емітентів із зовнішніх центрів сертифікації (CA). Це потрібно, бо багато серверів CA не додають заголовків CORS у відповіді, що унеможливлює пряме отримання з браузера.
Крім того, багато серверів CA віддають сертифікати через звичайний HTTP. Коли ваш екземпляр BentoPDF розміщено через HTTPS, браузери блокують такі HTTP-запити (політика змішаного контенту). CORS-проксі вирішує обидві проблеми, спрямовуючи запити через HTTPS-ендпоінт із потрібними заголовками.
## Як це працює
Під час підписання PDF сертифікатом:
1. Бібліотека `zgapdfsigner` намагається побудувати повний ланцюжок сертифікатів
2. Вона отримує сертифікати емітентів за URL, вбудованими в розширення AIA (Authority Information Access) вашого сертифіката
3. Ці запити проходять через CORS-проксі, який додає потрібні заголовки `Access-Control-Allow-Origin`
4. Проксі повертає дані сертифіката в браузер
## Самостійне розгортання CORS-проксі
Якщо ви самостійно розгортаєте BentoPDF, вам потрібно розгорнути власний CORS-проксі, щоб цифрові підписи працювали із сертифікатами, які потребують отримання ланцюжка.
### Варіант 1: Cloudflare Workers (рекомендовано)
1. **Встановіть Wrangler CLI**:
```bash
npm install -g wrangler
```
2. **Увійдіть у Cloudflare**:
```bash
wrangler login
```
3. **Клонуйте BentoPDF і змініть дозволені origin**:
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf/cloudflare
```
Відкрийте `cors-proxy-worker.js` і змініть масив `ALLOWED_ORIGINS` на свій домен:
```js
const ALLOWED_ORIGINS = [
'https://your-domain.com',
'https://www.your-domain.com',
];
```
::: warning Важливо
Без цієї зміни проксі відхилятиме всі запити з вашого сайту з помилкою **403 Forbidden**. За замовчуванням дозволено лише запити з `bentopdf.com`.
:::
4. **Розгорніть проксі**:
```bash
wrangler deploy
```
Запам'ятайте URL вашого worker (напр., `https://bentopdf-cors-proxy.your-subdomain.workers.dev`).
5. **Перезберіть BentoPDF з URL проксі**:
Якщо використовуєте Docker:
```bash
export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf .
```
Якщо збираєте з вихідного коду:
```bash
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
```
### Варіант 2: власний бекенд-проксі
Ви також можете створити власний ендпоінт проксі. Вимоги такі:
1. Приймати GET-запити з параметром запиту `url`
2. Отримувати URL зі свого сервера (на сервері обмежень CORS немає)
3. Повертати відповідь із такими заголовками:
- `Access-Control-Allow-Origin: https://your-domain.com`
- `Access-Control-Allow-Methods: GET, OPTIONS`
- `X-Content-Type-Options: nosniff`
Приклад реалізації на Express.js:
```javascript
app.get('/api/cert-proxy', async (req, res) => {
const targetUrl = req.query.url;
// Validate it's a certificate URL
if (!isValidCertUrl(targetUrl)) {
return res.status(400).json({ error: 'Invalid URL' });
}
try {
const response = await fetch(targetUrl);
const data = await response.arrayBuffer();
res.set('Access-Control-Allow-Origin', 'https://your-domain.com');
res.set('Content-Type', 'application/octet-stream');
res.set('X-Content-Type-Options', 'nosniff');
res.send(Buffer.from(data));
} catch (error) {
res.status(500).json({ error: 'Proxy error' });
}
});
```
## Міркування щодо безпеки
Вбудований Cloudflare Worker має кілька заходів безпеки:
| Функція | Опис |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| **Перевірка origin** | Дозволяє запити лише з доменів зі списку `ALLOWED_ORIGINS` |
| **Обмеження URL** | Дозволяє лише URL сертифікатів (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`, `/crl`) |
| **Блокування приватних IP** | Блокує приватні діапазони IPv4/IPv6, link-local, loopback, десяткові IP та метадані хмари |
| **Безпечний Content-Type** | Повертає лише безпечні MIME-типи сертифікатів, блокує підміну content-type від джерела |
| **Ліміт розміру файлу** | Передає відповідь потоком з лімітом 10MB, перериває завантаження, якщо його перевищено |
| **Обмеження частоти** | 60 запитів на хвилину на IP або IPv6 /64 (потрібен KV, у разі помилок KV пропускає запити) |
| **Підписи HMAC** | Необов'язкове підписування на стороні клієнта (стримує випадкові зловживання) |
### DNS rebinding (самостійне розгортання поза Cloudflare)
Worker відхиляє будь-який URL, що вказує на приватну або внутрішню IP-адресу. Він не може використовувати фіксований список дозволених хостів, бо URL ланцюжків сертифікатів вказують на будь-який CA, що видав сертифікат (FNMT, корпоративні CA тощо), тож призначення наперед невідомі. Через це залишається невелике вікно для rebinding: той, хто керує DNS власного домену, може віддати публічну IP під час перевірки безпеки, а приватну в реальному запиті за мить.
На Cloudflare це не експлуатується: Workers не мають доступу до приватних IP чи ендпоінта метаданих `169.254.169.254`, тож rebind нікуди не веде. Просто не давайте worker шляху всередину: не підключайте Tunnel, Service чи binding приватної мережі (йому потрібен лише `RATE_LIMIT_KV`).
Якщо ви запускаєте це поза Cloudflare (workerd, Node тощо) усередині VPC, середовище _може_ звертатися до внутрішніх IP. Поставте перед ним egress-фаєрвол і заблокуйте приватні діапазони: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16` та їхні IPv6-еквіваленти. Код застосунку не може цього забезпечити поза Cloudflare.
## Вимкнення проксі
Якщо ви не хочете використовувати CORS-проксі, задайте змінній середовища порожній рядок:
```
VITE_CORS_PROXY_URL=
```
**Примітка**: Без проксі підписання сертифікатами, що потребують отримання зовнішнього ланцюжка (як-от FNMT чи деякі корпоративні CA), завершиться помилкою "Failed to fetch".
## Усунення несправностей
### "Signing error: TypeError: Failed to fetch"
Зазвичай це означає одне з двох:
1. **CORS-проксі не налаштовано**: задайте `VITE_CORS_PROXY_URL` і перезберіть
2. **Змішаний контент заблоковано**: ваш сайт працює через HTTPS, а URL емітента сертифіката через HTTP. CORS-проксі це вирішує.
3. **CORS-проксі відхиляє ваш origin**: перевірте, чи ваш домен є в масиві `ALLOWED_ORIGINS` у `cors-proxy-worker.js`
### "403 Forbidden" від проксі
Вашого домену немає у списку `ALLOWED_ORIGINS`. Відредагуйте `cors-proxy-worker.js`:
```js
const ALLOWED_ORIGINS = ['https://your-domain.com'];
```
Потім розгорніть заново: `npx wrangler deploy`
### Тестування проксі
```bash
curl -H "Origin: https://your-domain.com" \
"https://your-proxy.workers.dev?url=http://www.cert.fnmt.es/certs/ACUSU.crt"
```
### Сертифікати, що працюють без проксі
Деякі сертифікати містять повний ланцюжок у файлі P12/PFX і не потребують зовнішнього отримання:
- Самопідписані сертифікати
- Деякі комерційні CA, що включають проміжні сертифікати
- Сертифікати, які ви вручну зібрали з повним ланцюжком
+622
View File
@@ -0,0 +1,622 @@
# Розгортання з Docker / Podman
Найпростіший спосіб самостійно розгорнути BentoPDF у робочому середовищі.
> [!IMPORTANT]
> **Обов'язкові заголовки для конвертації офісних файлів**
>
> Інструменти на основі LibreOffice (конвертація Word, Excel, PowerPoint) потребують таких HTTP-заголовків для підтримки `SharedArrayBuffer`:
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
>
> Сторінка також має віддаватися з безпечного контексту. `http://localhost` підходить для локального тестування, але `http://192.168.x.x` та інші LAN-адреси зазвичай не відповідають вимогам, тому конвертація Office через звичайний HTTP не працюватиме, навіть якщо заголовки присутні.
>
> Офіційні образи контейнерів містять ці заголовки. Якщо ви використовуєте зворотний проксі (Traefik, Caddy тощо), переконайтеся, що ці заголовки зберігаються або додаються, і використовуйте HTTPS для доступу не з loopback.
> [!TIP]
> **Користувачам Podman:** усі команди `docker` працюють з Podman, якщо замінити `docker` на `podman`, а `docker-compose` на `podman-compose`.
## Швидкий старт
> [!TIP]
> BentoPDF постачається у двох збірках:
>
> - **Self-Hosted build**: `ghcr.io/alam00000/bentopdf-simple:latest`. Усі PDF-інструменти, **без** маркетингових елементів (без головного екрана, FAQ, відгуків і футера). Використовуйте для внутрішніх розгортань у командах і організаціях. Це **не** урізана "lite"-версія.
> - **Commercial build**: `ghcr.io/alam00000/bentopdf:latest`. Повний маркетинговий сайт, який використовує сам bentopdf.com і власники комерційної ліцензії для публічних розгортань.
>
> Якщо вагаєтеся, беріть Self-Hosted build.
```bash
# Docker
docker run -d \
--name bentopdf \
-p 3000:8080 \
--restart unless-stopped \
ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -d \
--name bentopdf \
-p 3000:8080 \
ghcr.io/alam00000/bentopdf-simple:latest
```
## Docker Compose / Podman Compose
Створіть `docker-compose.yml`:
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
container_name: bentopdf
ports:
- '3000:8080'
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:8080']
interval: 30s
timeout: 10s
retries: 3
```
Запустіть:
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
## Збірка власного образу
```dockerfile
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginxinc/nginx-unprivileged:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
```
Зберіть і запустіть:
```bash
docker build -t bentopdf:custom .
docker run -d -p 3000:8080 bentopdf:custom
```
## Змінні середовища
| Змінна | Опис | Типове значення |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `SIMPLE_MODE` | Зібрати варіант Self-Hosted (ховає маркетингові елементи bentopdf.com: головний екран, FAQ, відгуки, футер). Усі PDF-інструменти залишаються. | `false` |
| `BASE_URL` | Розгортання в підкаталозі | `/` |
| `VITE_WASM_PYMUPDF_URL` | URL модуля PyMuPDF WASM | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
| `VITE_WASM_GS_URL` | URL модуля Ghostscript WASM | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` |
| `VITE_WASM_CPDF_URL` | URL модуля CoherentPDF WASM | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` |
| `VITE_TESSERACT_WORKER_URL` | URL скрипта OCR worker | _(порожньо; CDN Tesseract.js за замовчуванням)_ |
| `VITE_TESSERACT_CORE_URL` | Каталог основного runtime OCR | _(порожньо; CDN Tesseract.js за замовчуванням)_ |
| `VITE_TESSERACT_LANG_URL` | Каталог traineddata OCR | _(порожньо; CDN Tesseract.js за замовчуванням)_ |
| `VITE_TESSERACT_AVAILABLE_LANGUAGES` | Мови OCR через кому, що відображаються в інтерфейсі | _(порожньо; показати повний каталог)_ |
| `VITE_OCR_FONT_BASE_URL` | Каталог шрифтів текстового шару OCR | _(порожньо; використовувати віддалені URL шрифтів Noto)_ |
| `VITE_DEFAULT_LANGUAGE` | Типова мова інтерфейсу | `en` |
| `VITE_BRAND_NAME` | Власна назва бренду | `BentoPDF` |
| `VITE_BRAND_LOGO` | Шлях до логотипа відносно `public/` | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | Власний текст футера/авторських прав | `© 2026 BentoPDF. All rights reserved.` |
| `DISABLE_TOOLS` | ID інструментів через кому, які потрібно сховати | _(порожньо; усі інструменти ввімкнено)_ |
| `DISABLE_GITHUB_STARS` | Пропустити отримання кількості зірок GitHub під час завантаження сторінки та прибрати `api.github.com` із CSP. Збірки Simple Mode завжди це пропускають. | `false` |
| `SITE_URL` | Публічний URL, що записується в canonical, hreflang і посилання sitemap. Залиште типовим, якщо не хочете, щоб пошукові системи сприймали ваше розгортання як окремий сайт. | `https://www.bentopdf.com` |
| `ALLOW_INDEXING` | Із власним `SITE_URL` кожну сторінку позначено `noindex`, щоб ваша копія не конкурувала з bentopdf.com у пошуку. Задайте `true`, щоб дозволити пошуковим системам її індексувати. | `false` |
URL модулів WASM попередньо налаштовано з типовими значеннями CDN, тому всі розширені функції працюють одразу. Перевизначте їх для ізольованих (air-gapped) або самостійно розгорнутих середовищ.
### Пошукові системи
За замовчуванням кожна сторінка вказує пошуковим системам на bentopdf.com як на оригінал, тож ваше розгортання не з'являється як дублікат. Для приватного чи внутрішнього екземпляра нічого змінювати не потрібно.
- **Повністю сховати екземпляр із результатів пошуку:** запустіть контейнер із `-e ROBOTS_NOINDEX=true`. Для цього потрібен лише перезапуск, без перезбирання.
- **Зробити екземпляр окремим сайтом у результатах пошуку:** зберіть із `--build-arg SITE_URL=https://your-domain.example --build-arg ALLOW_INDEXING=true`. Без `ALLOW_INDEXING=true` власний `SITE_URL` позначає кожну сторінку як `noindex`.
### Content-Security-Policy
Образ nginx постачається із заголовком `Content-Security-Policy` у режимі примусового застосування. Директиви CSP `script-src`, `connect-src` і `font-src` **генеруються під час збірки** зі змінних URL `VITE_*` вище: усі хости, які ви передаєте через `--build-arg`, автоматично додаються до політики.
Отже:
- Якщо ви перевизначаєте `VITE_CORS_PROXY_URL` або `VITE_WASM_*_URL` під час збірки, CSP автоматично дозволяє ці origin, додаткове налаштування не потрібне.
- Для URL на `cdn.jsdelivr.net` `script-src` дозволяє лише точну версійну папку пакета, наприклад `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/`, а не весь CDN. jsDelivr віддає будь-який публічний пакет npm або репозиторій GitHub, тож дозвіл усього хоста дав би змогу запускати будь-який із них на вашому екземплярі. URL на будь-яких інших хостах дозволяються за origin.
- Якщо ви налаштовуєте власні URL WASM під час _роботи_ через сторінку «Додаткові налаштування» у застосунку, ці origin **не** потрапляють до CSP, і браузер блокуватиме запити до них. Це також стосується іншої версії того самого пакета на `cdn.jsdelivr.net`, бо дозволено лише версію, задану під час збірки. Налаштування під час роботи призначене для експериментів; для постійних власних URL задайте відповідний build arg `VITE_*`.
- Ізольовані розгортання, що перевизначають усі три значення `VITE_WASM_*_URL`, також позбуваються цих папок пакетів `cdn.jsdelivr.net` у CSP (кожне типове значення замінюється, а не доповнюється). Типові значення Tesseract OCR на `cdn.jsdelivr.net` залишаються в політиці, доки ви також не задасте три значення `VITE_TESSERACT_*`. Так само задання `VITE_CORS_PROXY_URL` замінює публічне типове значення `bentopdf-cors-proxy.bentopdf.workers.dev`.
CSP містить `'unsafe-eval'` у `script-src`, тому що runtime LibreOffice WASM (використовується інструментами конвертації Word/Excel/PowerPoint) і бібліотека зображень wasm-vips (використовується для PDF у TIFF) компілюють внутрішній диспетчерський код через `new Function()`. Його видалення зламало б ці інструменти. Якщо ви збираєте в `SIMPLE_MODE` (без LibreOffice) і не потребуєте PDF у TIFF, можна вручну відредагувати згенерований `security-headers.conf`, прибравши `'unsafe-eval'` для суворішої політики.
Для OCR залиште змінні `VITE_TESSERACT_*` порожніми, щоб використовувати типові онлайн-ресурси, або задайте всі три разом для самостійного/офлайн OCR. Часткові перевизначення OCR відхиляються, бо worker, основний runtime і каталог traineddata мають збігатися. Для повністю офлайн-виводу пошукового PDF також задайте `VITE_OCR_FONT_BASE_URL`, щоб шрифти текстового шару OCR завантажувалися з вашого внутрішнього сервера, а не з публічних URL шрифтів Noto.
`VITE_DEFAULT_LANGUAGE` задає мову інтерфейсу для нових відвідувачів. Підтримувані значення: `en`, `ar`, `be`, `fr`, `de`, `es`, `zh`, `zh-TW`, `vi`, `tr`, `id`, `it`, `pt`, `nl`, `da`. Користувачі й далі можуть змінювати мову: це змінює лише типове значення.
Приклад:
```bash
# Build with French as the default language
docker build --build-arg VITE_DEFAULT_LANGUAGE=fr -t bentopdf .
docker run -d -p 3000:8080 bentopdf
```
### Власний брендинг
Замініть типовий логотип BentoPDF, назву та текст футера на свої. Помістіть файл логотипа в папку `public/` (або використайте наявне зображення), а потім передайте змінні брендингу під час збірки:
```bash
docker build \
--build-arg VITE_BRAND_NAME="AcmePDF" \
--build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
--build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
-t acmepdf .
```
Брендинг працює і в повному режимі, і в Simple Mode, і його можна поєднувати з усіма іншими параметрами збірки.
### Вимкнення окремих інструментів
Ховайте інструменти з інтерфейсу з міркувань відповідності вимогам або безпеки. Вимкнені інструменти прибираються з головної сторінки, результатів пошуку, клавіатурних скорочень і конструктора робочих процесів. Прямий доступ за URL показує сторінку "інструмент недоступний".
ID інструмента це URL сторінки без `.html`. Наприклад, якщо інструмент розташований за адресою `bentopdf.com/edit-pdf.html`, ID це `edit-pdf`.
#### Як знайти ID інструментів
Найпростіше: відкрийте будь-який інструмент у BentoPDF і подивіться на URL. Остання частина шляху (без `.html`) це ID інструмента.
<details>
<summary>Повний список ID інструментів</summary>
**Редагування й анотації:** `edit-pdf`, `bookmark`, `table-of-contents`, `page-numbers`, `add-page-labels`, `bates-numbering`, `add-watermark`, `header-footer`, `invert-colors`, `scanner-effect`, `adjust-colors`, `background-color`, `text-color`, `sign-pdf`, `add-stamps`, `remove-annotations`, `crop-pdf`, `form-filler`, `form-creator`, `remove-blank-pages`
**Конвертація в PDF:** `image-to-pdf`, `jpg-to-pdf`, `png-to-pdf`, `webp-to-pdf`, `svg-to-pdf`, `bmp-to-pdf`, `heic-to-pdf`, `tiff-to-pdf`, `txt-to-pdf`, `markdown-to-pdf`, `json-to-pdf`, `csv-to-pdf`, `rtf-to-pdf`, `odt-to-pdf`, `word-to-pdf`, `excel-to-pdf`, `powerpoint-to-pdf`, `xps-to-pdf`, `mobi-to-pdf`, `epub-to-pdf`, `fb2-to-pdf`, `cbz-to-pdf`, `wpd-to-pdf`, `wps-to-pdf`, `xml-to-pdf`, `pages-to-pdf`, `odg-to-pdf`, `ods-to-pdf`, `odp-to-pdf`, `pub-to-pdf`, `vsd-to-pdf`, `psd-to-pdf`, `email-to-pdf`
**Конвертація з PDF:** `pdf-to-jpg`, `pdf-to-png`, `pdf-to-webp`, `pdf-to-bmp`, `pdf-to-tiff`, `pdf-to-cbz`, `pdf-to-svg`, `pdf-to-csv`, `pdf-to-excel`, `pdf-to-greyscale`, `pdf-to-json`, `pdf-to-docx`, `extract-images`, `pdf-to-markdown`, `prepare-pdf-for-ai`, `pdf-to-text`
**Організація й керування:** `ocr-pdf`, `merge-pdf`, `alternate-merge`, `organize-pdf`, `add-attachments`, `extract-attachments`, `edit-attachments`, `pdf-multi-tool`, `pdf-layers`, `extract-tables`, `split-pdf`, `divide-pages`, `extract-pages`, `delete-pages`, `add-blank-page`, `reverse-pages`, `rotate-pdf`, `rotate-custom`, `n-up-pdf`, `pdf-booklet`, `combine-single-page`, `view-metadata`, `edit-metadata`, `pdf-to-zip`, `compare-pdfs`, `posterize-pdf`, `page-dimensions`
**Оптимізація й відновлення:** `compress-pdf`, `pdf-to-pdfa`, `fix-page-size`, `linearize-pdf`, `remove-restrictions`, `repair-pdf`, `rasterize-pdf`, `deskew-pdf`, `font-to-outline`
**Безпека:** `encrypt-pdf`, `sanitize-pdf`, `decrypt-pdf`, `flatten-pdf`, `remove-metadata`, `change-permissions`, `digital-sign-pdf`, `validate-signature-pdf`, `timestamp-pdf`
</details>
#### Варіант 1: під час збірки (build arg Docker)
```bash
docker build \
--build-arg DISABLE_TOOLS="edit-pdf,sign-pdf,encrypt-pdf" \
-t bentopdf .
```
Це вшиває список вимкнених інструментів у JavaScript-бандл. Для зміни потрібне перезбирання.
#### Варіант 2: під час роботи (config.json)
Змонтуйте файл `config.json` у каталог, що віддається, перезбирання не потрібне:
```json
{
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
}
```
```bash
docker run -d \
-p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf-simple:latest
```
Або з Docker Compose:
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
ports:
- '3000:8080'
volumes:
- ./config.json:/usr/share/nginx/html/config.json:ro
```
Обидва способи можна поєднувати: списки об'єднуються. Якщо інструмент є в будь-якому з них, його вимкнено.
#### Вимкнення функцій редактора
Ви також можете вимкнути окремі функції всередині PDF-редактора (наприклад, редагування (redaction), анотації, форми), не вимикаючи весь інструмент редактора. Додайте `editorDisabledCategories` до `config.json`:
```json
{
"editorDisabledCategories": ["redaction", "annotation-stamp"]
}
```
<details>
<summary>Повний список категорій редактора</summary>
**Масштаб:** `zoom`, `zoom-in`, `zoom-out`, `zoom-fit-page`, `zoom-fit-width`, `zoom-marquee`, `zoom-level`
**Анотації:** `annotation`, `annotation-markup`, `annotation-highlight`, `annotation-underline`, `annotation-strikeout`, `annotation-squiggly`, `annotation-ink`, `annotation-text`, `annotation-stamp`
**Фігури:** `annotation-shape`, `annotation-rectangle`, `annotation-circle`, `annotation-line`, `annotation-arrow`, `annotation-polygon`, `annotation-polyline`
**Форми:** `form`, `form-textfield`, `form-checkbox`, `form-radio`, `form-select`, `form-listbox`, `form-fill-mode`
**Приховування даних:** `redaction`, `redaction-area`, `redaction-text`, `redaction-apply`, `redaction-clear`
**Документ:** `document`, `document-open`, `document-close`, `document-print`, `document-capture`, `document-export`, `document-fullscreen`, `document-protect`
**Сторінка:** `page`, `spread`, `rotate`, `scroll`, `navigation`
**Панель:** `panel`, `panel-sidebar`, `panel-search`, `panel-comment`
**Інструменти:** `tools`, `pan`, `pointer`, `capture`
**Виділення:** `selection`, `selection-copy`
**Історія:** `history`, `history-undo`, `history-redo`
</details>
Категорії ієрархічні: вимкнення батьківської (наприклад, `annotation`) вимикає всі її дочірні.
### Вимкнення лічильника зірок GitHub
Навігаційна панель показує актуальну кількість зірок GitHub, яку отримують з `api.github.com` під час завантаження сторінки. Це єдиний зовнішній запит, що не обслуговує жодну функцію PDF. Вимкніть його під час збірки, щоб прибрати код отримання з бандла та `api.github.com` із Content-Security-Policy:
```bash
docker build --build-arg DISABLE_GITHUB_STARS=true -t bentopdf .
```
Збірки Simple Mode (`bentopdf-simple`) завжди пропускають лічильник зірок: цей прапорець потрібен лише для Commercial build або власних повних збірок.
### Власні URL WASM (ізольовані / самостійно розгорнуті середовища)
> [!IMPORTANT]
> URL WASM вшиваються в JavaScript **під час збірки**. Файли WASM завантажує **браузер користувача** під час роботи: Docker не завантажує їх під час збірки. Для ізольованих мереж потрібно розмістити файли WASM на внутрішньому сервері, доступному для браузерів.
**Повний процес для ізольованого середовища:**
```bash
# 1. On a machine WITH internet — download WASM packages
bash scripts/prepare-airgap.sh --list-ocr-languages
bash scripts/prepare-airgap.sh --search-ocr-language german
# 2. Download WASM/OCR packages
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf
npm pack tesseract.js@7.0.0
npm pack tesseract.js-core@7.0.0
mkdir -p tesseract-langdata
curl -fsSL https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz -o tesseract-langdata/eng.traineddata.gz
mkdir -p ocr-fonts
curl -fsSL https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf -o ocr-fonts/NotoSans-Regular.ttf
# 3. Build the image with your internal server URLs
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data \
--build-arg VITE_TESSERACT_AVAILABLE_LANGUAGES=eng,deu \
--build-arg VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts \
-t bentopdf .
# 4. Export the image
docker save bentopdf -o bentopdf.tar
# 5. Transfer bentopdf.tar + the .tgz packages + tesseract-langdata/ + ocr-fonts/ into the air-gapped network
# 6. Inside the air-gapped network — load and run
docker load -i bentopdf.tar
# Extract WASM packages to your internal web server
mkdir -p /var/www/wasm/pymupdf /var/www/wasm/gs /var/www/wasm/cpdf /var/www/wasm/ocr/core /var/www/wasm/ocr/lang-data /var/www/wasm/ocr/fonts
tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C /var/www/wasm/pymupdf --strip-components=1
tar xzf bentopdf-gs-wasm-*.tgz -C /var/www/wasm/gs --strip-components=1
tar xzf coherentpdf-*.tgz -C /var/www/wasm/cpdf --strip-components=1
TEMP_TESS=$(mktemp -d)
tar xzf tesseract.js-7.0.0.tgz -C "$TEMP_TESS"
cp "$TEMP_TESS/package/dist/worker.min.js" /var/www/wasm/ocr/worker.min.js
rm -rf "$TEMP_TESS"
tar xzf tesseract.js-core-7.0.0.tgz -C /var/www/wasm/ocr/core --strip-components=1
cp ./tesseract-langdata/*.traineddata.gz /var/www/wasm/ocr/lang-data/
cp ./ocr-fonts/* /var/www/wasm/ocr/fonts/
# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf
```
Для `--ocr-languages` використовуйте коди, які виводить `bash scripts/prepare-airgap.sh --list-ocr-languages`, або шукайте за назвою через `bash scripts/prepare-airgap.sh --search-ocr-language <term>`. Коли ви збираєте з обмеженим набором мов OCR, передайте ті самі коди в `VITE_TESSERACT_AVAILABLE_LANGUAGES`, щоб застосунок показував лише вбудовані мови. Для повного офлайн-виводу OCR також розмістіть вбудований каталог `ocr-fonts/` і вкажіть на нього `VITE_OCR_FONT_BASE_URL`.
Задайте змінній порожній рядок, щоб вимкнути відповідний модуль (користувачі мають налаштувати його вручну через «Додаткові налаштування»).
## Власний порт
За замовчуванням BentoPDF слухає порт `8080` усередині контейнера. Щоб його змінити, задайте змінну середовища `PORT` під час запуску:
```bash
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
```
| Змінна | Опис | Типове значення |
| ------ | ------------------------------------ | --------------- |
| `PORT` | Порт, який слухає Nginx у контейнері | `8080` |
Це працює і зі стандартним, і з nonroot Dockerfile.
## Власний ID користувача (PUID/PGID)
Для середовищ, які вимагають запуску від конкретного користувача без прав root (NAS-пристрої, Kubernetes із контекстами безпеки, організаційні політики), BentoPDF надає окремий Dockerfile з підтримкою PUID/PGID у стилі LSIO.
### Збірка і запуск
```bash
# Build the non-root image
docker build -f Dockerfile.nonroot -t bentopdf-nonroot .
# Run with custom UID/GID
docker run -d \
--name bentopdf \
-p 3000:8080 \
-e PUID=1000 \
-e PGID=1000 \
--restart unless-stopped \
bentopdf-nonroot
```
### Змінні середовища
| Змінна | Опис | Типове значення |
| -------------- | -------------------------- | --------------- |
| `PUID` | ID користувача для запуску | `1000` |
| `PGID` | ID групи для запуску | `1000` |
| `PORT` | Порт, який слухає Nginx | `8080` |
| `DISABLE_IPV6` | Вимкнути слухача IPv6 | `false` |
### Docker Compose
```yaml
services:
bentopdf:
build:
context: .
dockerfile: Dockerfile.nonroot
container_name: bentopdf
ports:
- '3000:8080'
environment:
- PUID=1000
- PGID=1000
restart: unless-stopped
```
### Як це працює
Контейнер запускається від root, створює користувача із заданими PUID/PGID, налаштовує власника для всіх каталогів із правом запису, а потім скидає привілеї за допомогою `su-exec`. Процес nginx повністю працює від указаного вами користувача.
> [!NOTE]
> Стандартний `Dockerfile` використовує `nginx-unprivileged` (UID 101) і рекомендований для більшості розгортань. Використовуйте `Dockerfile.nonroot` лише тоді, коли потрібен конкретний UID/GID.
> [!WARNING]
> PUID/PGID не можуть бути `0` (root). Точка входу перевіряє вхідні дані та завершиться з помилкою для недійсних значень.
## З Traefik (зворотний проксі)
```yaml
services:
traefik:
image: traefik:v2.10
command:
- '--providers.docker=true'
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--certificatesresolvers.letsencrypt.acme.email=you@example.com'
- '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
- '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
ports:
- '80:80'
- '443:443'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.bentopdf.rule=Host(`pdf.example.com`)'
- 'traefik.http.routers.bentopdf.entrypoints=websecure'
- 'traefik.http.routers.bentopdf.tls.certresolver=letsencrypt'
- 'traefik.http.services.bentopdf.loadbalancer.server.port=8080'
# Required headers for SharedArrayBuffer (LibreOffice WASM)
- 'traefik.http.routers.bentopdf.middlewares=bentopdf-headers'
- 'traefik.http.middlewares.bentopdf-headers.headers.customresponseheaders.Cross-Origin-Opener-Policy=same-origin'
- 'traefik.http.middlewares.bentopdf-headers.headers.customresponseheaders.Cross-Origin-Embedder-Policy=require-corp'
restart: unless-stopped
```
## З Caddy (зворотний проксі)
```yaml
services:
caddy:
image: caddy:2
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
restart: unless-stopped
volumes:
caddy_data:
```
Caddyfile:
```
pdf.example.com {
reverse_proxy bentopdf:8080
header Cross-Origin-Opener-Policy "same-origin"
header Cross-Origin-Embedder-Policy "require-corp"
}
```
## Обмеження ресурсів
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
```
## Podman Quadlet (інтеграція із systemd)
[Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) дає змогу запускати контейнери Podman як сервіси systemd. Це ідеально для робочих розгортань на системах Linux.
### Базове налаштування Quadlet
Створіть файл юніта контейнера в `~/.config/containers/systemd/bentopdf.container` (користувацький) або `/etc/containers/systemd/bentopdf.container` (системний):
```ini
[Unit]
Description=BentoPDF - Privacy-first PDF toolkit
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
```
### Увімкнення і запуск
```bash
# Reload systemd to detect new unit
systemctl --user daemon-reload
# Start the service
systemctl --user start bentopdf
# Enable on boot
systemctl --user enable bentopdf
# Check status
systemctl --user status bentopdf
# View logs
journalctl --user -u bentopdf -f
```
> [!TIP]
> Для розгортання на рівні системи використовуйте `systemctl` без прапорця `--user` і помістіть файл у `/etc/containers/systemd/`.
### Quadlet із перевіркою стану
```ini
[Unit]
Description=BentoPDF with health monitoring
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
HealthCmd=wget --spider -q http://localhost:8080 || exit 1
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
```
### Автоматичне оновлення з Quadlet
Podman може автоматично оновлювати контейнери, коли з'являються нові образи:
```bash
# Enable auto-update timer
systemctl --user enable --now podman-auto-update.timer
# Check for updates manually
podman auto-update
# Dry run (check without updating)
podman auto-update --dry-run
```
### Налаштування мережі Quadlet
Для власної конфігурації мережі створіть файл мережі `bentopdf.network`:
```ini
[Network]
Subnet=10.89.0.0/24
Gateway=10.89.0.1
```
Потім послайтеся на нього у файлі контейнера:
```ini
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
Network=bentopdf.network
```
## Оновлення
```bash
# Pull latest image
docker compose pull
# Recreate container
docker compose up -d
```
+266
View File
@@ -0,0 +1,266 @@
# Розгортання на Hostinger
[Hostinger](https://hostinger.com) це популярний провайдер спільного хостингу. Цей посібник описує розгортання BentoPDF на спільному хостингу Hostinger.
## Вимоги
- Тарифний план Hostinger з доступом до файлового менеджера
- Локально встановлений Node.js для збірки
## Крок 1: зберіть проєкт
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
Зібрані файли будуть у папці `dist`.
## Крок 2: завантажте на Hostinger
### Розгортання на кореневому домені
1. Увійдіть у свій акаунт Hostinger
2. Перейдіть до **File Manager** → **public_html**
3. **Видаліть** усі наявні файли (за потреби зробіть резервну копію)
4. **Завантажте** весь вміст локальної папки `dist` до `public_html`
### Розгортання в підкаталозі
Якщо розгортаєте в підкаталозі (напр., `yourdomain.com/pdf-tools/`):
1. Зберіть із правильним базовим URL:
```bash
BASE_URL=/pdf-tools/ npm run build
```
2. Створіть папку в Hostinger:
- Перейдіть до **File Manager** → **public_html**
- Створіть нову папку: `pdf-tools`
3. Завантажте весь вміст `dist` до `public_html/pdf-tools/`
## Крок 3: створіть файл .htaccess
Створіть файл `.htaccess` у корені вашої папки розгортання (`public_html` або `public_html/pdf-tools/`):
::: warning Важливо
Для розгортання в підкаталозі змініть `RewriteBase /` на `RewriteBase /pdf-tools/` (або назву вашої папки).
:::
```apache
RewriteEngine On
RewriteBase /
# ============================================
# 1. SECURITY HEADERS (CRITICAL FOR WASM)
# ============================================
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# REQUIRED for soffice.js (SharedArrayBuffer)
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
</IfModule>
# ============================================
# 2. BROWSER CACHING
# ============================================
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType application/gzip "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
# ============================================
# 3. COMPRESSION (STANDARD)
# ============================================
# Prevent server from double-compressing files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
SetEnvIfNoCase Request_URI "\.br$" no-gzip
SetEnvIfNoCase Request_URI "\.wasm$" no-gzip
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff
AddOutputFilterByType DEFLATE font/ttf
</IfModule>
# ============================================
# 4. MIME TYPES & SPECIAL FILE HANDLING
# ============================================
AddType application/javascript .js .mjs
AddType application/wasm .wasm
AddType font/woff2 .woff2
AddType font/woff .woff
AddType image/webp .webp
# Handle soffice.wasm.gz correctly
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
Header set Cross-Origin-Resource-Policy "cross-origin"
Header append Vary Accept-Encoding
</FilesMatch>
# Handle data.gz
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
Header append Vary Accept-Encoding
</FilesMatch>
# ============================================
# 5. REDIRECTS & ROUTING
# ============================================
# Canonical WWW (update domain as needed)
RewriteCond %{HTTP_HOST} ^bentopdf\.com [NC]
RewriteRule ^(.*)$ https://www.bentopdf.com/$1 [L,R=301]
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Remove trailing slash (except for language root directories)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/$
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# 5.1. LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root (for SEO consistency)
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# 5.5. DOCS ROUTING (VitePress)
# ============================================
RewriteCond %{REQUEST_URI} ^/docs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.*)$ $1.html [L]
# ============================================
# 6. ADD .HTML EXTENSION IF FILE EXISTS (ROOT LEVEL ONLY)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
# ============================================
# 7. ERROR PAGES
# ============================================
ErrorDocument 404 /404.html
```
## Приклад .htaccess для підкаталогу
Для `yourdomain.com/pdf-tools/` змініть ці рядки:
```apache
RewriteBase /pdf-tools/
# ... (same content as above, but update paths) ...
# Language prefix root
RewriteCond %{DOCUMENT_ROOT}/pdf-tools/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf-tools/$1/index.html [L]
# Language prefix with path
RewriteCond %{DOCUMENT_ROOT}/pdf-tools/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf-tools/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf-tools/404.html
```
## Усунення несправностей
### Файли WASM не завантажуються
Переконайтеся, що MIME-типи правильно задано в `.htaccess`:
```apache
AddType application/wasm .wasm
```
### Інструменти LibreOffice не працюють
Заголовки безпеки критично важливі для SharedArrayBuffer:
```apache
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
```
Якщо заголовки не застосовуються, зверніться до підтримки Hostinger, щоб увімкнути `mod_headers`.
### Помилки 404 під час оновлення сторінки
Переконайтеся, що правило розширення `.html` і мовні маршрути налаштовано правильно. BentoPDF використовує статичні HTML-файли, а не маршрутизацію SPA:
```apache
# Language routes serve actual files from language directories
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# Root level pages
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
```
### Ліміти завантаження файлів
Hostinger може мати обмеження розміру файлів. Якщо масове завантаження не вдається, завантажуйте файли WASM окремо.
+538
View File
@@ -0,0 +1,538 @@
# Посібник із самостійного розміщення
BentoPDF можна розмістити на власній інфраструктурі. Цей посібник описує різні варіанти розгортання.
## Швидкий старт із Docker / Podman
Найшвидший спосіб розмістити BentoPDF самостійно:
> [!TIP]
> BentoPDF постачається у двох збірках:
>
> - **Збірка для самостійного розміщення** (Self-Hosted): `ghcr.io/alam00000/bentopdf-simple:latest`. Усі PDF-інструменти, **без** маркетингового оформлення (без hero-блоку, FAQ, відгуків і футера). Використовуйте її для внутрішніх розгортань у команді чи організації. Це **не** урізана "lite"-версія.
> - **Комерційна збірка** (Commercial): `ghcr.io/alam00000/bentopdf:latest`. Повний маркетинговий сайт, який використовує сам bentopdf.com і власники комерційної ліцензії з публічними розгортаннями.
>
> Якщо вагаєтеся, беріть збірку для самостійного розміщення.
> [!IMPORTANT]
> Перетворення офісних файлів потребує `SharedArrayBuffer`, а отже застосунок має бути і ізольованим між походженнями (cross-origin isolated), і обслуговуватися з безпечного контексту. Офіційний образ уже надсилає потрібні заголовки COOP/COEP, але браузери все одно вимикають `SharedArrayBuffer` на звичайних HTTP-адресах локальної мережі, як-от `http://192.168.x.x`.
>
> Використовуйте `http://localhost` лише для тестування на одному пристрої. Якщо користувачі відкривають BentoPDF через LAN IP або ім’я хоста, налаштуйте HTTPS.
```bash
# Docker
docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
```
Або з Docker Compose / Podman Compose:
```yaml
# docker-compose.yml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
ports:
- '3000:8080'
restart: unless-stopped
```
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
## Podman Quadlet (Linux Systemd)
Запустіть BentoPDF як службу systemd. Створіть `~/.config/containers/systemd/bentopdf.container`:
```ini
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=default.target
```
```bash
systemctl --user daemon-reload
systemctl --user enable --now bentopdf
```
Повну документацію щодо Quadlet дивіться в [посібнику з розгортання Docker](/uk/self-hosting/docker).
## Збірка з вихідного коду
```bash
# Clone and build
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
# The built files are in the `dist` folder
```
## Параметри конфігурації
### Збірка для самостійного розміщення (Simple Mode)
Збірка для самостійного розміщення (образ `bentopdf-simple`, також званий Simple Mode) **функціонально ідентична** комерційній збірці: усі PDF-інструменти на місці й працюють однаково. Вона лише приховує маркетингове оформлення, яке має сенс тільки на самому bentopdf.com або в комерційному публічному розгортанні. **Це не урізана чи "lite"-версія.**
**Що приховує збірка для самостійного розміщення** (лише косметика, жодні функції PDF не вилучаються):
- Панель навігації, hero-секцію, секцію переваг, FAQ, відгуки, футер
- Змінює заголовок сторінки на "PDF інструменти"
**Що зберігає збірка для самостійного розміщення** (усе, що справді працює з PDF):
- Усі PDF-інструменти (об’єднання, розділення, редагування, підпис, OCR, перетворення офісних файлів тощо)
- Підтримку власного брендингу, усі налаштування часу збірки та виконання
Комерційна збірка (`ghcr.io/alam00000/bentopdf:latest`) живить сам bentopdf.com і використовується власниками комерційної ліцензії з публічними розгортаннями. Вона додає hero-блок, FAQ, відгуки та футер, які не мають сенсу у внутрішньому інструменті.
Якщо ви розміщуєте BentoPDF для своєї команди, організації або як внутрішній інструмент, беріть збірку для самостійного розміщення:
```bash
# Use the pre-built image (recommended)
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Or build it yourself
SIMPLE_MODE=true npm run build
```
Повну інформацію дивіться в [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md).
### Комерційна збірка (публічні розгортання з вашим брендом)
Комерційна збірка (образ `bentopdf` без суфікса `-simple`) живить сам bentopdf.com. Вона містить повний маркетинговий сайт (hero, переваги, FAQ, відгуки, футер) на додачу до всіх PDF-інструментів. Використовуйте цю збірку, коли хочете **розгорнути BentoPDF як публічний PDF-сервіс під власним брендом**, наприклад:
- Ви запускаєте BentoPDF як хмарний сервіс (SaaS) для кінцевих користувачів на власному домені
- Вам потрібен досвід лендингу (маркетингові секції + інструменти), а не лише голий набір інструментів
- Ви власник комерційної ліцензії та вбудовуєте BentoPDF у комерційний продукт
**Запуск як є** (бренд BentoPDF, зручно для перегляду збірки):
```bash
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
```
**Збірка з власним брендом** (типовий комерційний сценарій):
```bash
docker build \
--build-arg VITE_BRAND_NAME="AcmePDF" \
--build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
--build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. All rights reserved." \
-t acmepdf .
docker run -p 3000:8080 acmepdf
```
Усі інші параметри часу збірки (`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, перевизначення URL для WASM тощо) працюють так само, як і в збірці для самостійного розміщення.
::: warning Ліцензування
Запуск комерційної збірки дозволений за будь-яким із двох варіантів ліцензії BentoPDF:
- **AGPL-3.0** (безкоштовно): дозволено, якщо ваше розгортання публікує повний вихідний код під AGPL, включно з будь-якими змінами брендингу та навколишньою бізнес-логікою.
- **Комерційна ліцензія** ($79 довічно): потрібна для закритих/пропрієтарних розгортань, де ви не відкриваєте вихідний код свого форка з брендингом чи бізнес-код.
Повне порівняння дивіться на [сторінці ліцензування](https://www.bentopdf.com/licensing). WASM-модулі під AGPL (PyMuPDF, Ghostscript, CoherentPDF) завантажуються з CDN під час виконання, тому вони не потрапляють у ваш образ і не змінюють вашого ліцензійного статусу.
:::
### Базовий URL
Розгортання в підкаталозі:
```bash
BASE_URL=/pdf-tools/ npm run build
```
### Власний брендинг
Замініть стандартний логотип, назву й текст футера BentoPDF на власні під час збірки:
| Змінна | Опис | За замовчуванням |
| ------------------ | ------------------------------------ | --------------------------------------- |
| `VITE_BRAND_NAME` | Назва бренду в заголовку та футері | `BentoPDF` |
| `VITE_BRAND_LOGO` | Шлях до логотипа відносно `public/` | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | Власний текст футера/авторських прав | `© 2026 BentoPDF. All rights reserved.` |
```bash
# Place your logo in public/, then build
VITE_BRAND_NAME="AcmePDF" \
VITE_BRAND_LOGO="images/acme-logo.svg" \
VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
npm run build
```
Або через Docker:
```bash
docker build \
--build-arg VITE_BRAND_NAME="AcmePDF" \
--build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
--build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
-t acmepdf .
```
Брендинг працює і в повному режимі, і в Simple Mode та поєднується з усіма іншими параметрами часу збірки (`BASE_URL`, `SIMPLE_MODE`, `VITE_DEFAULT_LANGUAGE`).
### Вимкнення окремих інструментів
Приховуйте окремі інструменти з міркувань відповідності вимогам або безпеки. Вимкнені інструменти вилучаються з головної сторінки, пошуку, ярликів, конструктора робочих процесів і прямого доступу за URL.
Ідентифікатори інструментів є URL сторінки без `.html`: відкрийте будь-який інструмент і подивіться на URL (наприклад, `edit-pdf`, `sign-pdf`, `encrypt-pdf`).
**Під час збірки** (вбудовується в бандл):
```bash
DISABLE_TOOLS="edit-pdf,sign-pdf" npm run build
```
**Під час виконання** (без перебудови, змонтуйте `config.json`):
```json
{
"disabledTools": ["edit-pdf", "sign-pdf"]
}
```
```bash
docker run -d -p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf-simple:latest
```
Обидва способи можна поєднувати: списки об’єднуються.
Також можна вимкнути окремі функції всередині редактора PDF (наприклад, редагування вмісту) без вимкнення всього інструмента, додавши `editorDisabledCategories` до `config.json`. Повний перелік категорій дивіться в [посібнику з Docker](/uk/self-hosting/docker#вимкнення-функціи-редактора).
## Посібники з розгортання
Виберіть свою платформу:
- [Vercel](/uk/self-hosting/vercel)
- [Netlify](/uk/self-hosting/netlify)
- [Cloudflare Pages](/uk/self-hosting/cloudflare)
- [AWS S3 + CloudFront](/uk/self-hosting/aws)
- [Hostinger](/uk/self-hosting/hostinger)
- [Nginx](/uk/self-hosting/nginx)
- [Apache](/uk/self-hosting/apache)
- [Docker](/uk/self-hosting/docker)
- [Kubernetes](/uk/self-hosting/kubernetes)
- [CORS-проксі](/uk/self-hosting/cors-proxy) - потрібен для цифрових підписів
## Поширені проблеми
### Перетворення Word / ODT / Excel / PowerPoint у PDF зависає (SharedArrayBuffer недоступний)
**Симптом**: перетворення документів через LibreOffice (Word, ODT, Excel, PowerPoint у PDF) зависає приблизно на 55% або не запускається. У консолі браузера може бути:
> `ReferenceError: SharedArrayBuffer is not defined`
...або `window.crossOriginIsolated` повертає `false`, або компіляція WASM повідомляє `expected magic word 00 61 73 6d, found 1f 8b 08 08`.
**Причина**: LibreOffice WASM потребує `SharedArrayBuffer`, який браузер вмикає лише тоді, коли сторінка **ізольована між походженнями** І обслуговується з **безпечного контексту**. Тобто мають виконуватися дві умови:
1. Кожна відповідь містить обидва заголовки:
- `Cross-Origin-Embedder-Policy: require-corp`
- `Cross-Origin-Opener-Policy: same-origin`
2. Сторінка обслуговується через `https://...` або `http://localhost`. Звичайний HTTP на LAN IP (наприклад, `http://192.168.x.x`) безпечним НЕ вважається: браузери вимикають там `SharedArrayBuffer`.
Невідповідність `00 61 73 6d / 1f 8b 08 08` є окремим підсимптомом: у попередньо стиснутих файлів `.wasm.gz` / `.data.gz` відсутній заголовок відповіді `Content-Encoding: gzip`, тож браузер отримує сирі байти gzip замість розпакованого WASM.
**Рішення**: точну конфігурацію дивіться в посібнику для вашої платформи:
- [Nginx →](/uk/self-hosting/nginx#не-працює-перетворення-word-odt-excel-у-pdf)
- [Apache →](/uk/self-hosting/apache#конвертація-word-odt-excel-у-pdf-не-працює)
- [AWS S3 + CloudFront →](/uk/self-hosting/aws#крок-3b-політика-заголовків-відповіді-обов-язково-для-libreoffice-wasm)
- [Cloudflare Pages →](/uk/self-hosting/cloudflare#фаил-конфігураціі) (файл `_headers`)
- [Netlify →](/uk/self-hosting/netlify#перетворення-word-odt-excel-у-pdf-зависає-на-55) (`netlify.toml`)
- [Vercel →](/uk/self-hosting/vercel#не-працює-перетворення-word-odt-excel-у-pdf) (`vercel.json`)
- [Hostinger →](/uk/self-hosting/hostinger#інструменти-libreoffice-не-працюють) (`.htaccess`)
- [Kubernetes →](/uk/self-hosting/kubernetes#як-переконатися-що-заголовки-sharedarraybuffer-працюють-ingress-gateway)
- **Docker**: обробляється автоматично вбудованою конфігурацією nginx, жодних дій не потрібно.
**Перевірка**: відкрийте консоль DevTools на будь-якій сторінці BentoPDF і виконайте:
```js
console.log(window.crossOriginIsolated); // should be true
console.log(typeof SharedArrayBuffer); // should be "function"
```
Якщо сторінка працює через HTTPS або `http://localhost` І обидва заголовки COEP/COOP присутні в кожній відповіді, обидві перевірки пройдуть. Якщо ви на `http://192.168.x.x` чи іншому не-loopback HTTP-походженні, налаштуйте HTTPS: рішення лише через заголовки не існує.
### Файли `.mjs` віддаються як `application/octet-stream`
**Симптом**: Sign PDF / Form Filler / деякі інші інструменти показують порожній переглядач або не завантажуються. Консоль браузера повідомляє:
> `Failed to load module script: The server responded with a non-JavaScript MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.`
**Причина**: у вашого вебсервера або зворотного проксі немає зіставлення MIME-типу для файлів `.mjs` (вбудований PDF-переглядач постачає ES-модулі з таким розширенням). Багато стандартних конфігурацій серверів за замовчуванням віддають невідомі розширення як `application/octet-stream`, і браузери відмовляються виконувати їх як ES-модулі.
**Рішення**: точний фрагмент дивіться в посібнику для вашої платформи:
- [Nginx →](/uk/self-hosting/nginx#sign-pdf-або-form-filler-показує-порожніи-переглядач-помилка-mime-для-mjs)
- [Apache →](/uk/self-hosting/apache#sign-pdf-або-form-filler-показує-порожніи-переглядач-помилка-mime-для-mjs)
- [AWS S3 + CloudFront →](/uk/self-hosting/aws#крок-2-зберіть-і-завантажте) (дивіться `aws s3 cp ... --include "*.mjs"`)
- [Kubernetes →](/uk/self-hosting/kubernetes#помилки-mime-типу-mjs-порожніи-iframe-у-sign-pdf-form-filler)
- **Docker, Vercel, Netlify, Cloudflare Pages, Hostinger**: обробляється автоматично, жодних дій не потрібно.
**Перевірка**: відкрийте DevTools → вкладка Network, знайдіть запит `.mjs`, що завершився помилкою, і перевірте заголовок відповіді `Content-Type`. Він має бути `application/javascript`. Якщо там досі `application/octet-stream`, зовнішній зворотний проксі чи CDN, можливо, повторно визначає тип: перевірте кожен рівень у ланцюжку обслуговування.
## Конфігурація WASM (компоненти під AGPL)
BentoPDF **не вбудовує** бібліотеки обробки під ліцензією AGPL у свій вихідний код, але **попередньо налаштовує URL-адреси CDN**, тож усі функції працюють одразу, без ручного налаштування.
::: tip Нульова конфігурація за замовчуванням
Починаючи з v2.0.0 модулі WASM попередньо налаштовані на завантаження з CDN jsDelivr через змінні середовища. Усі розширені функції працюють одразу без жодного налаштування користувачем.
:::
| Компонент | Ліцензія | Функції |
| --------------- | -------- | ------------------------------------------------------------------ |
| **PyMuPDF** | AGPL-3.0 | Перетворення EPUB/MOBI/FB2/XPS, витяг зображень, витяг таблиць |
| **Ghostscript** | AGPL-3.0 | Перетворення в PDF/A, стиснення, вирівнювання нахилу, растеризація |
| **CoherentPDF** | AGPL-3.0 | Зміст, вкладення, об’єднання PDF із закладками |
### Змінні середовища за замовчуванням
Вони задані в `.env.production` і вбудовані в збірку:
```bash
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/
VITE_TESSERACT_WORKER_URL=
VITE_TESSERACT_CORE_URL=
VITE_TESSERACT_LANG_URL=
VITE_TESSERACT_AVAILABLE_LANGUAGES=
VITE_OCR_FONT_BASE_URL=
```
### Перевизначення URL для WASM
Значення за замовчуванням можна перевизначити під час збірки для власних розгортань:
```bash
# Via Docker build args
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://your-server.com/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://your-server.com/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://your-server.com/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://your-server.com/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://your-server.com/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://your-server.com/ocr/lang-data \
--build-arg VITE_TESSERACT_AVAILABLE_LANGUAGES=eng,deu \
--build-arg VITE_OCR_FONT_BASE_URL=https://your-server.com/ocr/fonts \
-t bentopdf .
# Or via .env.production before building from source
VITE_WASM_PYMUPDF_URL=https://your-server.com/pymupdf/ npm run build
```
Щоб повністю вимкнути модуль (вимагається ручне налаштування користувачем через «Додаткові налаштування»), задайте його змінній порожній рядок.
Для OCR або залиште всі змінні `VITE_TESSERACT_*` порожніми й використовуйте онлайнові ресурси за замовчуванням, або задайте URL воркера/ядра/мовних даних разом для самостійно розміщеного/офлайн OCR. Якщо ви вбудовуєте лише окремі мови OCR, також задайте `VITE_TESSERACT_AVAILABLE_LANGUAGES` тими самими кодами через кому, щоб інтерфейс пропонував лише встановлені мови, а непідтримувані вибори завершувалися описовою помилкою. Для повністю офлайн-створення PDF із пошуком також задайте `VITE_OCR_FONT_BASE_URL` як внутрішній каталог, що віддає вбудовані шрифти OCR.
Користувачі також можуть будь-коли перевизначити ці значення через **Додаткові налаштування** в інтерфейсі: перевизначення, збережені в браузері, мають пріоритет над значеннями середовища за замовчуванням.
### Розгортання в ізольованій мережі / офлайн
Для мереж без доступу до інтернету (державні установи, охорона здоров’я, фінанси тощо). URL-адреси WASM вбудовуються в JavaScript під час **збірки**, а самі файли WASM завантажує **браузер користувача** під час виконання. Тому все потрібно підготувати на машині з інтернетом, а потім перенести в ізольовану мережу.
#### Автоматичний скрипт (рекомендовано)
Скрипт `prepare-airgap.sh` автоматизує весь процес: завантажує пакети WASM, збирає образ Docker і створює самодостатній пакет зі скриптом налаштування.
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
# Show supported OCR language codes (for --ocr-languages)
bash scripts/prepare-airgap.sh --list-ocr-languages
# Search OCR language codes by name or abbreviation
bash scripts/prepare-airgap.sh --search-ocr-language german
# Interactive mode — prompts for all options
bash scripts/prepare-airgap.sh
# Or fully automated
bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm
```
Це створює каталог із пакетом:
```
bentopdf-airgap-bundle/
bentopdf.tar # Docker image
*.tgz # WASM packages (PyMuPDF, Ghostscript, CoherentPDF, Tesseract)
tesseract-langdata/ # OCR traineddata files
ocr-fonts/ # OCR text-layer font files
setup.sh # Setup script for the air-gapped side
README.md # Instructions
```
Перенесіть пакет в ізольовану мережу через USB, внутрішнє сховище артефактів або інший схвалений спосіб. Потім запустіть вбудований скрипт налаштування:
```bash
cd bentopdf-airgap-bundle
bash setup.sh
```
Скрипт налаштування завантажує образ Docker, розпаковує файли WASM і за бажанням запускає контейнер.
**Параметри скрипта:**
| Прапорець | Опис | За замовчуванням |
| ------------------------------ | ----------------------------------------------------------------- | ---------------------------------------- |
| `--wasm-base-url <url>` | Де внутрішньо розміщуватимуться WASM | _(обов’язково, запитується, якщо немає)_ |
| `--image-name <name>` | Тег образу Docker | `bentopdf` |
| `--output-dir <path>` | Каталог пакета на виході | `./bentopdf-airgap-bundle` |
| `--simple-mode` | Увімкнути Simple Mode | вимк. |
| `--base-url <path>` | Базовий URL підкаталогу (наприклад, `/pdf/`) | `/` |
| `--language <code>` | Мова інтерфейсу за замовчуванням (наприклад, `fr`, `de`) | _(немає)_ |
| `--brand-name <name>` | Власна назва бренду | _(немає)_ |
| `--brand-logo <path>` | Шлях до логотипа відносно `public/` | _(немає)_ |
| `--footer-text <text>` | Власний текст футера | _(немає)_ |
| `--ocr-languages <list>` | Мови OCR для включення, через кому | `eng` |
| `--list-ocr-languages` | Вивести підтримувані коди й назви OCR та вийти | вимк. |
| `--search-ocr-language <term>` | Пошук кодів OCR за назвою або абревіатурою | вимк. |
| `--dockerfile <path>` | Dockerfile для використання | `Dockerfile` |
| `--skip-docker` | Пропустити збірку й експорт Docker | вимк. |
| `--skip-wasm` | Пропустити завантаження WASM (повторно використати наявні `.tgz`) | вимк. |
Інтерактивний запит також приймає `list` для виводу повного списку підтримуваних кодів Tesseract і `search <term>` для пошуку збігів, наприклад `search german` або `search chi`.
::: warning Вимога одного походження
Файли WASM мають обслуговуватися з **того самого походження**, що й застосунок BentoPDF. Веб-воркери використовують `importScripts()`, який не може завантажувати скрипти з іншого походження. Наприклад, якщо BentoPDF працює на `https://internal.example.com`, базовий URL WASM також має бути `https://internal.example.com/wasm`.
:::
#### Ручні кроки
<details>
<summary>Якщо ви хочете зробити все вручну без скрипта</summary>
**Крок 1: Завантажте пакети WASM і OCR** (на машині з інтернетом)
```bash
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf
npm pack tesseract.js@7.0.0
npm pack tesseract.js-core@7.0.0
mkdir -p tesseract-langdata
curl -fsSL https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz -o tesseract-langdata/eng.traineddata.gz
mkdir -p ocr-fonts
curl -fsSL https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf -o ocr-fonts/NotoSans-Regular.ttf
```
**Крок 2: Зберіть образ Docker із внутрішніми URL**
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data \
--build-arg VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts \
-t bentopdf .
```
**Крок 3: Експортуйте образ Docker**
```bash
docker save bentopdf -o bentopdf.tar
```
**Крок 4: Перенесіть в ізольовану мережу**
Скопіюйте через USB, внутрішнє сховище артефактів або інший схвалений спосіб передачі:
- `bentopdf.tar`: образ Docker
- П’ять пакетів `.tgz` WASM/OCR з кроку 1
- Каталог `tesseract-langdata/` з кроку 1
- Каталог `ocr-fonts/` з кроку 1
**Крок 5: Налаштуйте всередині ізольованої мережі**
```bash
# Load the Docker image
docker load -i bentopdf.tar
# Extract WASM packages
mkdir -p ./wasm/pymupdf ./wasm/gs ./wasm/cpdf ./wasm/ocr/core ./wasm/ocr/lang-data ./wasm/ocr/fonts
tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C ./wasm/pymupdf --strip-components=1
tar xzf bentopdf-gs-wasm-*.tgz -C ./wasm/gs --strip-components=1
tar xzf coherentpdf-*.tgz -C ./wasm/cpdf --strip-components=1
TEMP_TESS=$(mktemp -d)
tar xzf tesseract.js-7.0.0.tgz -C "$TEMP_TESS"
cp "$TEMP_TESS/package/dist/worker.min.js" ./wasm/ocr/worker.min.js
rm -rf "$TEMP_TESS"
tar xzf tesseract.js-core-7.0.0.tgz -C ./wasm/ocr/core --strip-components=1
cp ./tesseract-langdata/*.traineddata.gz ./wasm/ocr/lang-data/
cp ./ocr-fonts/* ./wasm/ocr/fonts/
# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf
```
Переконайтеся, що файли доступні за URL-адресами, налаштованими на кроці 2, зокрема `.../ocr/worker.min.js`, `.../ocr/core`, `.../ocr/lang-data` і `.../ocr/fonts`.
</details>
::: info Збираєте з вихідного коду замість Docker?
Задайте змінні в `.env.production` перед запуском `npm run build`:
```bash
VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/
VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/
VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/
VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js
VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core
VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data
VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts
```
:::
### Розміщення власного WASM-проксі
Якщо потрібно віддавати файли WASM під AGPL із правильними заголовками CORS, можна розгорнути простий проксі. Приклад реалізації дивіться в [посібнику Cloudflare WASM Proxy](https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md).
::: tip Навіщо розділення?
Таке розділення забезпечує:
- Чітку юридичну відповідність для комерційних користувачів
- Збереження ядра BentoPDF під його подвійною ліцензією (AGPL-3.0 / Commercial)
- Завантаження файлів WASM під час виконання, а не вбудовування у вихідний код
:::
## Системні вимоги
| Вимога | Мінімум |
| ------- | ------------------------------- |
| Сховище | ~100 МБ (ядро без модулів AGPL) |
| RAM | 512 МБ |
| CPU | Будь-який сучасний процесор |
::: tip
BentoPDF є статичним сайтом: база даних чи серверна частина не потрібні!
:::
+211
View File
@@ -0,0 +1,211 @@
# Розгортання в Kubernetes
Kubernetes може бути надмірним для статичного сайту, але він чудово підходить, якщо ви вже стандартизувалися на Helm + GitOps.
> [!IMPORTANT]
> **Обов’язкові заголовки для перетворення офісних файлів**
>
> Інструменти на основі LibreOffice (перетворення Word, Excel, PowerPoint) потребують таких HTTP-заголовків для підтримки `SharedArrayBuffer`:
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
>
> Офіційні образи BentoPDF на nginx містять ці заголовки. У Kubernetes **контролери Ingress/Gateway також є зворотними проксі**, тож переконайтеся, що ці заголовки зберігаються (або додайте їх на межі).
## Вимоги
- Кластер Kubernetes
- Helm v3
- Образ BentoPDF на nginx (наприклад, `ghcr.io/alam00000/bentopdf:<tag>`), що обслуговує **порт 8080**
## Розгортання через Helm
### Встановлення з цього репозиторію (локальний чарт)
```bash
kubectl create namespace bentopdf
helm upgrade --install bentopdf /path/to/bentopdf/chart \
--namespace bentopdf \
--set image.repository=ghcr.io/alam00000/bentopdf \
--set image.tag=latest
```
### Встановлення з GHCR (OCI-чарт)
Якщо чарт опубліковано в GHCR як артефакт OCI:
```bash
export GHCR_USERNAME="<github-org-or-user>"
helm upgrade --install bentopdf oci://ghcr.io/$GHCR_USERNAME/charts/bentopdf \
--namespace bentopdf \
--create-namespace \
--version 0.1.0 \
--set image.repository=ghcr.io/alam00000/bentopdf \
--set image.tag=latest
```
## Відкриття доступу
### Port-forward (швидка перевірка)
```bash
kubectl -n bentopdf port-forward deploy/bentopdf 8080:8080
```
### Ingress (необов’язково)
Увімкніть Ingress (приклад для nginx-ingress):
```yaml
ingress:
enabled: true
className: nginx
hosts:
- host: pdf.example.com
paths:
- path: /
pathType: Prefix
```
### Gateway API (необов’язково)
Цей чарт підтримує Gateway API `Gateway` + `HTTPRoute`.
Приклад (оператор Cloudflare Gateway API):
```yaml
gateway:
enabled: true
name: bento-tunnel
namespace: bentopdf
gatewayClassName: cloudflare
httpRoute:
enabled: true
parentRefs:
- name: bento-tunnel
namespace: bentopdf
sectionName: http
hostnames:
- pdfs.example.com
```
## Як переконатися, що заголовки SharedArrayBuffer працюють (Ingress/Gateway)
### Що "має" відбуватися
Конфігурація nginx у BentoPDF встановлює потрібні заголовки відповіді. Більшість контролерів Ingress/Gateway **передають заголовки відповіді вихідного сервера без змін**.
### Що може це зламати
- Політика контролера/межі, що **перевизначає** або **видаляє** заголовки відповіді
- Проміжне ПЗ "security headers", яке встановлює інші значення COOP/COEP
### Як перевірити
Виконайте це для свого публічного endpoint:
```bash
curl -I https://pdf.example.com/ | egrep -i 'cross-origin-opener-policy|cross-origin-embedder-policy'
```
Ви маєте побачити:
- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`
### Якщо ваш контролер Ingress їх не зберігає
Додайте заголовки на межі (залежить від контролера). Приклад для **nginx-ingress**:
```yaml
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
```
## Помилки MIME-типу `.mjs` (порожній iframe у Sign PDF / Form Filler)
Якщо консоль браузера показує `Failed to load module script: ... non-JavaScript MIME type "application/octet-stream"` для запиту `.mjs`, то контролер Ingress/Gateway перед nginx BentoPDF підмінює заголовок `Content-Type` вихідного сервера на `application/octet-stream`.
nginx в образі BentoPDF уже віддає `.mjs` як `application/javascript`. Рішення: заборонити контролеру видаляти/замінювати його. Для nginx-ingress:
```yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
location ~* \.mjs$ {
types {} default_type application/javascript;
}
```
Або скористайтеся фільтром `ResponseHeaderModifier` у Gateway API, щоб встановити `Content-Type: application/javascript` для шляхів `.mjs`. Перевірте у DevTools → вкладка Network: невдалий запит `.mjs` тепер має показувати `content-type: application/javascript`.
### Якщо ви використовуєте Gateway API і хочете примусово додати заголовки
Gateway API підтримує фільтр `ResponseHeaderModifier`. Його можна підключити в `httpRoute.rules[*].filters`:
```yaml
httpRoute:
enabled: true
hostnames: [pdf.example.com]
parentRefs:
- name: bento-tunnel
namespace: misc
sectionName: http
rules:
- matches:
- path: { type: PathPrefix, value: / }
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: Cross-Origin-Opener-Policy
value: same-origin
- name: Cross-Origin-Embedder-Policy
value: require-corp
```
Підтримка окремих фільтрів залежить від вашого контролера Gateway; якщо фільтр ігнорується, додайте заголовки на рівні межі/контролера.
## Вимкнення окремих інструментів
Використайте ConfigMap, щоб вимкнути інструменти під час виконання без перебудови образу:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bentopdf-config
namespace: bentopdf
data:
config.json: |
{
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
}
```
Змонтуйте його в каталог, що обслуговується:
```yaml
spec:
containers:
- name: bentopdf
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/config.json
subPath: config.json
readOnly: true
volumes:
- name: config
configMap:
name: bentopdf-config
```
Ідентифікатори інструментів є URL сторінки без `.html`: відкрийте будь-який інструмент і подивіться на URL (наприклад, `edit-pdf`, `merge-pdf`, `compress-pdf`). Вимкнені інструменти приховуються на головній сторінці, у пошуку, ярликах, конструкторі робочих процесів і при прямому доступі за URL. Повний перелік параметрів дивіться в [посібнику з Docker](/uk/self-hosting/docker#вимкнення-окремих-інструментів).
+139
View File
@@ -0,0 +1,139 @@
# Розгортання у Netlify
[Netlify](https://netlify.com) пропонує чудовий хостинг статичних сайтів із щедрим безкоштовним тарифом.
## Розгортання в один клік
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/alam00000/bentopdf)
## Ручне розгортання
### Крок 1: Підключіть репозиторій
1. Увійдіть у [Netlify](https://app.netlify.com)
2. Натисніть "Add new site" → "Import an existing project"
3. Підключіть свій акаунт GitHub
4. Виберіть свій форк BentoPDF
### Крок 2: Налаштуйте параметри збірки
| Параметр | Значення |
| ----------------- | --------------- |
| Build command | `npm run build` |
| Publish directory | `dist` |
| Node version | 18+ |
### Крок 3: Розгортання
Натисніть "Deploy site" і дочекайтеся збірки.
## Файл конфігурації
Створіть `netlify.toml` у корені проєкту:
```toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "18"
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
[[headers]]
for = "/*"
[headers.values]
Cross-Origin-Embedder-Policy = "require-corp"
Cross-Origin-Opener-Policy = "same-origin"
Cross-Origin-Resource-Policy = "cross-origin"
# Pre-compressed LibreOffice WASM binary - must be served with Content-Encoding
[[headers]]
for = "/libreoffice-wasm/soffice.wasm.gz"
[headers.values]
Content-Type = "application/wasm"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
# Pre-compressed LibreOffice WASM data - must be served with Content-Encoding
[[headers]]
for = "/libreoffice-wasm/soffice.data.gz"
[headers.values]
Content-Type = "application/octet-stream"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
# Cache other WASM files
[[headers]]
for = "*.wasm"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
Content-Type = "application/wasm"
# SPA routing
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
::: warning Важливо
Заголовки `Cross-Origin-Embedder-Policy` і `Cross-Origin-Opener-Policy` потрібні для перетворення Word/ODT/Excel/PowerPoint у PDF. Без них `SharedArrayBuffer` недоступний, і рушій LibreOffice WASM не зможе ініціалізуватися.
Заголовки `Content-Encoding: gzip` для файлів `.wasm.gz` і `.data.gz` наказують браузеру автоматично їх розпакувати. Без них браузер отримує сирі байти gzip, і компіляція WASM завершується помилкою.
:::
## Змінні середовища
Задайте їх у Site settings → Environment variables:
| Змінна | Опис |
| ----------------------- | --------------------------------------------------------------------- |
| `SIMPLE_MODE` | Встановіть `true` для мінімальної збірки |
| `VITE_BRAND_NAME` | Власна назва бренду (замінює "BentoPDF") |
| `VITE_BRAND_LOGO` | Шлях до логотипа відносно `public/` (наприклад, `images/my-logo.svg`) |
| `VITE_FOOTER_TEXT` | Власний текст футера/авторських прав |
| `VITE_DEFAULT_LANGUAGE` | Мова інтерфейсу за замовчуванням (наприклад, `fr`, `de`, `es`) |
## Власний домен
1. Перейдіть до Site settings → Domain management
2. Натисніть "Add custom domain"
3. Дотримуйтеся інструкцій із налаштування DNS
## Великі медіафайли
Для великих файлів WASM розгляньте можливість увімкнути [Netlify Large Media](https://docs.netlify.com/large-media/overview/):
```bash
netlify lm:setup
git lfs track "*.wasm"
```
## Усунення неполадок
### Перетворення Word/ODT/Excel у PDF зависає на 55%
Якщо перетворення документів зависає на 55%, відкрийте консоль DevTools і перевірте:
```js
console.log(window.crossOriginIsolated); // should be true
console.log(typeof SharedArrayBuffer); // should be "function"
```
Якщо `crossOriginIsolated` дорівнює `false`, заголовки COEP/COOP з вашого `netlify.toml` не застосовуються. Переконайтеся, що файл лежить у корені проєкту, і розгорніть заново.
Якщо в консолі з’являється `expected magic word 00 61 73 6d, found 1f 8b 08 08`, у файлів `.wasm.gz` відсутні заголовки `Content-Encoding: gzip`. Переконайтеся, що блоки `[[headers]]` для `soffice.wasm.gz` і `soffice.data.gz` є у вашому `netlify.toml`.
### Збірка завершується помилкою
Перевірте сумісність версії Node:
```toml
[build.environment]
NODE_VERSION = "20"
```
### Повільне початкове завантаження
Увімкніть оптимізацію ресурсів у Site settings → Build & deploy → Asset optimization.
+232
View File
@@ -0,0 +1,232 @@
# Розгортання з Nginx
Розмістіть BentoPDF на власному сервері за допомогою Nginx.
## Вимоги
- Сервер Ubuntu/Debian
- Встановлений Nginx
- SSL-сертифікат (рекомендовано: Let's Encrypt)
## Крок 1: Зберіть проєкт
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
Щоб налаштувати брендинг, задайте змінні середовища перед збіркою:
```bash
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
```
## Крок 2: Скопіюйте файли
```bash
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## Крок 3: Конфігурація Nginx
Створіть `/etc/nginx/sites-available/bentopdf`:
```nginx
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
root /var/www/bentopdf;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/wasm;
gzip_min_length 1000;
# WASM MIME type
types {
application/wasm wasm;
}
# Required headers for SharedArrayBuffer (LibreOffice WASM)
# These must be on every response - especially HTML pages
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Pre-compressed LibreOffice WASM binary
location ~* /libreoffice-wasm/soffice\.wasm\.gz$ {
gzip off;
types {} default_type application/wasm;
add_header Content-Encoding gzip;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# Pre-compressed LibreOffice WASM data
location ~* /libreoffice-wasm/soffice\.data\.gz$ {
gzip off;
types {} default_type application/octet-stream;
add_header Content-Encoding gzip;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
}
```
## Крок 4: Увімкніть сайт
```bash
sudo ln -s /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
## Крок 5: SSL через Let's Encrypt
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
```
## Розгортання в підкаталозі
Щоб розмістити за адресою `/pdf/`:
```nginx
location /pdf/ {
alias /var/www/bentopdf/;
try_files $uri $uri/ /pdf/index.html;
}
```
Зберіть із:
```bash
BASE_URL=/pdf/ npm run build
```
## Налаштування продуктивності
Додайте до `nginx.conf`:
```nginx
http {
# Enable sendfile
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Increase buffer sizes
client_max_body_size 100M;
# Worker connections
worker_connections 2048;
}
```
## Усунення неполадок
### WASM не завантажується
Переконайтеся, що MIME-тип задано:
```nginx
types {
application/wasm wasm;
}
```
### Sign PDF або Form Filler показує порожній переглядач (помилка MIME для `.mjs`)
Якщо консоль браузера показує:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
...а запит, що завершується помилкою, закінчується на `.mjs`, то в nginx немає зіставлення `.mjs → application/javascript`. Вбудований PDF-переглядач (його використовують Sign PDF, Form Filler та кілька інших інструментів) постачає файли ES-модулів із розширенням `.mjs`, а стандартний `mime.types` nginx їх за замовчуванням не містить.
Виправте це, додавши `mjs` до наявного рядка `application/javascript` у `/etc/nginx/mime.types`:
```nginx
application/javascript js mjs;
```
> НЕ додавайте _другий_ рядок `application/javascript`: nginx розбирає `mime.types` рядок за рядком, і діє лише одне зіставлення типу з розширенням, тож другий рядок у деяких версіях мовчки ігнорується. Відредагуйте наявний рядок, додавши `mjs` поруч із `js`.
Або перевизначте локально всередині блоку server (без редагування глобального файлу):
```nginx
types {
application/javascript js mjs;
application/wasm wasm;
}
```
Перезавантажте nginx після зміни:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
Щоб підтвердити: у DevTools браузера → вкладка Network знайдіть запит `.mjs`, що завершився помилкою, і перевірте заголовок відповіді `Content-Type`. Тепер він має бути `application/javascript`, а не `application/octet-stream`.
### Не працює перетворення Word/ODT/Excel у PDF
LibreOffice WASM потребує `SharedArrayBuffer`, для якого потрібні заголовки `Cross-Origin-Embedder-Policy` і `Cross-Origin-Opener-Policy`. Також потрібен безпечний контекст, тож `http://localhost` підходить для локального тестування, а `http://192.168.x.x` та інші LAN-адреси зазвичай потребують HTTPS. Зверніть увагу, що директиви nginx `add_header` у блоці `location` **перевизначають** директиви `add_header` рівня server, а не поєднуються з ними. Кожен блок `location` із власним `add_header` має містити заголовки COEP/COOP.
Перевірте так:
```bash
curl -I https://your-domain.com/ | grep -i cross-origin
```
Якщо перед nginx стоїть зворотний проксі, переконайтеся, що він зберігає ці заголовки.
### 502 Bad Gateway
Перевірте журнали помилок Nginx:
```bash
sudo tail -f /var/log/nginx/error.log
```
+122
View File
@@ -0,0 +1,122 @@
# Розгортання у Vercel
[Vercel](https://vercel.com) пропонує найпростіше розгортання статичних сайтів.
## Розгортання в один клік
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/alam00000/bentopdf)
## Ручне розгортання
### Крок 1: Створіть форк репозиторію
Створіть форк [bentopdf/bentopdf](https://github.com/alam00000/bentopdf) у своєму акаунті GitHub.
### Крок 2: Імпортуйте у Vercel
1. Перейдіть на [vercel.com/new](https://vercel.com/new)
2. Виберіть свій форк репозиторію
3. Налаштуйте проєкт:
| Параметр | Значення |
| ---------------- | --------------- |
| Framework Preset | Vite |
| Build Command | `npm run build` |
| Output Directory | `dist` |
| Install Command | `npm install` |
### Крок 3: Змінні середовища (необов’язково)
Додайте їх за потреби:
| Змінна | Опис |
| ----------------------- | --------------------------------------------------------------------- |
| `SIMPLE_MODE` | Встановіть `true` для мінімального інтерфейсу |
| `VITE_BRAND_NAME` | Власна назва бренду (замінює "BentoPDF") |
| `VITE_BRAND_LOGO` | Шлях до логотипа відносно `public/` (наприклад, `images/my-logo.svg`) |
| `VITE_FOOTER_TEXT` | Власний текст футера/авторських прав |
| `VITE_DEFAULT_LANGUAGE` | Мова інтерфейсу за замовчуванням (наприклад, `fr`, `de`, `es`) |
### Крок 4: Розгортання
Натисніть "Deploy" і дочекайтеся завершення збірки.
## Власний домен
1. Перейдіть до налаштувань проєкту
2. Відкрийте "Domains"
3. Додайте свій домен
4. Налаштуйте DNS відповідно до інструкцій
## Обмеження
::: warning Великі файли
Серверні функції Vercel мають ліміт 50 МБ. Оскільки BentoPDF є статичним сайтом, це не повинно вплинути на вас, але модулі WASM великі (загалом ~100 МБ). Переконайтеся, що вони віддаються з папки `/public`.
:::
## Усунення неполадок
### Тайм-аут збірки
Якщо збірка завершується за тайм-аутом, спробуйте:
```json
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": "dist"
}
```
### Помилка 404 після оновлення сторінки
Додайте `vercel.json` для маршрутизації SPA:
```json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
```
### Не працює перетворення Word/ODT/Excel у PDF
Перетворення через LibreOffice WASM потребують `SharedArrayBuffer`, для якого на всіх сторінках потрібні такі заголовки відповіді:
- `Cross-Origin-Embedder-Policy: require-corp`
- `Cross-Origin-Opener-Policy: same-origin`
Попередньо стиснуті файли `.wasm.gz` і `.data.gz` також потребують `Content-Encoding: gzip`, щоб браузер їх розпакував.
Додайте це до свого `vercel.json`:
```json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" }
]
},
{
"source": "/libreoffice-wasm/soffice.wasm.gz",
"headers": [
{ "key": "Content-Type", "value": "application/wasm" },
{ "key": "Content-Encoding", "value": "gzip" }
]
},
{
"source": "/libreoffice-wasm/soffice.data.gz",
"headers": [
{ "key": "Content-Type", "value": "application/octet-stream" },
{ "key": "Content-Encoding", "value": "gzip" }
]
}
]
}
```
Щоб перевірити, відкрийте консоль DevTools і виконайте `console.log(window.crossOriginIsolated)`: має повернутися `true`.