mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-13 17:54:19 +01:00
feat: translate all pages, UI messages and docs and fix SEO redirects and indexing
This commit is contained in:
1 parent
5e019de5ae
commit
4edb1718cc
3344 files changed
+415643
-4759
No files matched your search
@@ -0,0 +1,275 @@
|
||||
# Triển khai với Apache
|
||||
|
||||
Lưu trữ BentoPDF bằng Apache HTTP Server.
|
||||
|
||||
## Yêu cầu
|
||||
|
||||
- Apache 2.4+
|
||||
- Đã bật mod_rewrite
|
||||
- Chứng chỉ SSL (khuyến nghị)
|
||||
|
||||
## Bước 1: Build dự án
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Để tùy chỉnh thương hiệu, đặt các biến môi trường trước khi build:
|
||||
|
||||
```bash
|
||||
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
|
||||
```
|
||||
|
||||
## Bước 2: Sao chép tệp
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/bentopdf
|
||||
sudo cp -r dist/* /var/www/bentopdf/
|
||||
sudo chown -R www-data:www-data /var/www/bentopdf
|
||||
```
|
||||
|
||||
## Bước 3: Cấu hình Apache
|
||||
|
||||
Tạo `/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>
|
||||
```
|
||||
|
||||
## Bước 4: .htaccess cho định tuyến
|
||||
|
||||
Tạo `/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
|
||||
```
|
||||
|
||||
## Bước 5: Bật các module cần thiết
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo a2enmod ssl
|
||||
sudo a2enmod headers
|
||||
sudo a2enmod expires
|
||||
sudo a2enmod deflate
|
||||
```
|
||||
|
||||
## Bước 6: Bật trang web
|
||||
|
||||
```bash
|
||||
sudo a2ensite bentopdf.conf
|
||||
sudo apache2ctl configtest
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
## Triển khai trong thư mục con
|
||||
|
||||
Để lưu trữ tại `/pdf/`:
|
||||
|
||||
1. Build với base URL:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf/ npm run build
|
||||
```
|
||||
|
||||
2. Cập nhật `.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
|
||||
```
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### Lỗi WASM 404
|
||||
|
||||
Đảm bảo đã cấu hình MIME type:
|
||||
|
||||
```apache
|
||||
AddType application/wasm .wasm
|
||||
```
|
||||
|
||||
### Ký PDF hoặc Form Filler hiển thị trình xem trống (lỗi MIME `.mjs`)
|
||||
|
||||
Nếu console của trình duyệt hiển thị:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
...và yêu cầu bị lỗi có đuôi `.mjs`, cấu hình Apache của bạn đang thiếu ánh xạ `.mjs → application/javascript`. Trình xem PDF đi kèm dùng các tệp ES module có đuôi `.mjs`, và `mime.types` mặc định của Apache không có sẵn đuôi này.
|
||||
|
||||
Thêm vào VirtualHost hoặc `.htaccess`:
|
||||
|
||||
```apache
|
||||
AddType application/javascript .mjs
|
||||
```
|
||||
|
||||
Sau đó tải lại Apache:
|
||||
|
||||
```bash
|
||||
sudo apache2ctl configtest && sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
Để xác nhận: trong DevTools của trình duyệt → thẻ Network, tìm yêu cầu `.mjs` bị lỗi và kiểm tra header phản hồi `Content-Type`. Giờ nó phải là `application/javascript`, không phải `application/octet-stream`.
|
||||
|
||||
### Rewrite không hoạt động
|
||||
|
||||
Kiểm tra mod_rewrite đã được bật:
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
```
|
||||
|
||||
### Chuyển Word/ODT/Excel sang PDF không hoạt động
|
||||
|
||||
LibreOffice WASM cần `SharedArrayBuffer`, và điều này đòi hỏi các header sau:
|
||||
|
||||
```apache
|
||||
Header always set Cross-Origin-Embedder-Policy "require-corp"
|
||||
Header always set Cross-Origin-Opener-Policy "same-origin"
|
||||
```
|
||||
|
||||
Nó cũng cần một ngữ cảnh bảo mật (secure context). `http://localhost` dùng được để thử nghiệm cục bộ, nhưng `http://192.168.x.x` hoặc các IP LAN khác thường cần HTTPS. Nếu các header đã có nhưng `window.crossOriginIsolated` vẫn là `false`, hãy kiểm tra xem trang có đang được mở qua HTTP thường trên một origin không phải loopback hay không.
|
||||
|
||||
Các tệp `.wasm.gz` và `.data.gz` nén sẵn cũng cần `Content-Encoding` đúng:
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
Đảm bảo `mod_headers` đã được bật: `sudo a2enmod headers`
|
||||
|
||||
### Bị từ chối quyền truy cập
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/bentopdf
|
||||
sudo chmod -R 755 /var/www/bentopdf
|
||||
```
|
||||
@@ -0,0 +1,200 @@
|
||||
# Triển khai lên AWS S3 + CloudFront
|
||||
|
||||
Lưu trữ BentoPDF trên AWS để kiểm soát và mở rộng tối đa.
|
||||
|
||||
## Kiến trúc
|
||||
|
||||
```
|
||||
User → CloudFront (CDN) → S3 (Static Files)
|
||||
```
|
||||
|
||||
## Bước 1: Tạo S3 bucket
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Bước 2: Build và tải lên
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Bước 3: Tạo bản phân phối CloudFront
|
||||
|
||||
```bash
|
||||
aws cloudfront create-distribution \
|
||||
--origin-domain-name your-bentopdf-bucket.s3.amazonaws.com \
|
||||
--default-root-object index.html
|
||||
```
|
||||
|
||||
Hoặc dùng AWS Console:
|
||||
|
||||
1. Vào CloudFront → Create distribution
|
||||
2. Origin domain: chọn S3 bucket của bạn
|
||||
3. Bật "Origin Access Control"
|
||||
4. Default root object: `index.html`
|
||||
5. Tạo distribution
|
||||
|
||||
## Bước 3b: Chính sách header phản hồi (bắt buộc cho LibreOffice WASM)
|
||||
|
||||
Các chuyển đổi dựa trên LibreOffice (Word, Excel, PowerPoint sang PDF) cần `SharedArrayBuffer`, và điều này đòi hỏi các header phản hồi cụ thể. Hãy tạo một CloudFront Response Headers Policy:
|
||||
|
||||
1. Vào CloudFront → Policies → Response headers
|
||||
2. Tạo chính sách tùy chỉnh với các header sau:
|
||||
|
||||
| Header | Giá trị |
|
||||
| ------------------------------ | -------------- |
|
||||
| `Cross-Origin-Embedder-Policy` | `require-corp` |
|
||||
| `Cross-Origin-Opener-Policy` | `same-origin` |
|
||||
| `Cross-Origin-Resource-Policy` | `cross-origin` |
|
||||
|
||||
3. Gắn chính sách vào default cache behavior của distribution
|
||||
|
||||
Hoặc qua 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}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Bước 3c: Metadata S3 cho tệp WASM nén sẵn
|
||||
|
||||
Các tệp WASM của LibreOffice được nén sẵn (`.wasm.gz`, `.data.gz`). Hãy đặt Content-Type và Content-Encoding đúng để trình duyệt giải nén chúng:
|
||||
|
||||
```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 Quan trọng
|
||||
Nếu thiếu chính sách header phản hồi, `SharedArrayBuffer` sẽ không khả dụng và các chuyển đổi LibreOffice WASM sẽ bị treo ở khoảng 55%. Nếu thiếu Content-Encoding đúng trên các tệp `.gz`, trình duyệt sẽ nhận byte gzip thô và việc biên dịch WASM sẽ thất bại.
|
||||
:::
|
||||
|
||||
## Bước 4: Chính sách S3 bucket
|
||||
|
||||
Cho phép CloudFront truy cập bucket:
|
||||
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Bước 5: Trang lỗi tùy chỉnh
|
||||
|
||||
Cấu hình 404 trả về `index.html` cho định tuyến SPA:
|
||||
|
||||
1. CloudFront → Error pages
|
||||
2. Tạo phản hồi lỗi tùy chỉnh:
|
||||
- HTTP error code: 404
|
||||
- Response page path: `/index.html`
|
||||
- HTTP response code: 200
|
||||
|
||||
## Ước tính chi phí
|
||||
|
||||
| Tài nguyên | Chi phí ước tính |
|
||||
| ------------------------ | ---------------- |
|
||||
| Lưu trữ S3 (~500MB) | ~$0.01/tháng |
|
||||
| CloudFront (truyền 1TB) | ~$85/tháng |
|
||||
| CloudFront (truyền 10GB) | ~$0.85/tháng |
|
||||
|
||||
::: tip
|
||||
Dùng S3 Intelligent Tiering để tối ưu chi phí cho các tệp ít được truy cập.
|
||||
:::
|
||||
|
||||
## Tự động hóa với 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,154 @@
|
||||
# Triển khai lên Cloudflare Pages
|
||||
|
||||
[Cloudflare Pages](https://pages.cloudflare.com) cung cấp dịch vụ lưu trữ trang tĩnh toàn cầu, nhanh, với băng thông không giới hạn.
|
||||
|
||||
## Triển khai nhanh
|
||||
|
||||
1. Vào [Cloudflare Pages](https://dash.cloudflare.com/?to=/:account/pages)
|
||||
2. Nhấp "Create a project"
|
||||
3. Kết nối kho GitHub của bạn
|
||||
|
||||
## Cấu hình build
|
||||
|
||||
| Cài đặt | Giá trị |
|
||||
| ---------------------- | --------------- |
|
||||
| Framework preset | None |
|
||||
| Build command | `npm run build` |
|
||||
| Build output directory | `dist` |
|
||||
| Root directory | `/` |
|
||||
|
||||
## Biến môi trường
|
||||
|
||||
Thêm các biến này trong Settings → Environment variables:
|
||||
|
||||
| Biến | Giá trị |
|
||||
| ----------------------- | ---------------------------------------------------- |
|
||||
| `NODE_VERSION` | `18` |
|
||||
| `SIMPLE_MODE` | `false` (tùy chọn) |
|
||||
| `VITE_BRAND_NAME` | Tên thương hiệu tùy chỉnh (tùy chọn) |
|
||||
| `VITE_BRAND_LOGO` | Đường dẫn logo tương đối so với `public/` (tùy chọn) |
|
||||
| `VITE_FOOTER_TEXT` | Văn bản chân trang/bản quyền tùy chỉnh (tùy chọn) |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Ngôn ngữ giao diện mặc định, ví dụ `fr` (tùy chọn) |
|
||||
|
||||
## Tệp cấu hình
|
||||
|
||||
Tạo `_headers` trong thư mục `public` của bạn:
|
||||
|
||||
```
|
||||
# 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 Quan trọng
|
||||
Các header `Cross-Origin-Embedder-Policy` và `Cross-Origin-Opener-Policy` là bắt buộc cho việc chuyển Word/ODT/Excel/PowerPoint sang PDF. Nếu thiếu, `SharedArrayBuffer` sẽ không khả dụng và engine LibreOffice WASM sẽ không khởi tạo được.
|
||||
:::
|
||||
|
||||
Tạo `_redirects` cho định tuyến SPA:
|
||||
|
||||
```
|
||||
/* /index.html 200
|
||||
```
|
||||
|
||||
## Tên miền tùy chỉnh
|
||||
|
||||
1. Vào dự án Pages của bạn
|
||||
2. Nhấp "Custom domains"
|
||||
3. Thêm tên miền của bạn
|
||||
4. Cloudflare sẽ tự động cấu hình DNS nếu tên miền nằm trên Cloudflare
|
||||
|
||||
## Ưu điểm
|
||||
|
||||
- **Băng thông không giới hạn, miễn phí**
|
||||
- **CDN toàn cầu** với hơn 300 điểm biên
|
||||
- **HTTPS tự động**
|
||||
- **Bản triển khai xem trước** cho pull request
|
||||
- **Build nhanh**
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### Tải lên tệp lớn
|
||||
|
||||
Cloudflare Pages hỗ trợ tệp tối đa 25 MB. Các module WASM thường ổn, nhưng nếu bạn chạm giới hạn, hãy cân nhắc:
|
||||
|
||||
```bash
|
||||
# Split large files during build
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Giới hạn kích thước Worker
|
||||
|
||||
Nếu dùng Cloudflare Workers cho định tuyến nâng cao, lưu ý giới hạn 1 MB cho gói miễn phí.
|
||||
|
||||
## CORS Proxy Worker (cho chữ ký số)
|
||||
|
||||
Công cụ Chữ ký số cần một CORS proxy để lấy chuỗi chứng chỉ. Hãy triển khai worker đi kèm:
|
||||
|
||||
```bash
|
||||
cd cloudflare
|
||||
npx wrangler login
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
### Tính năng bảo mật
|
||||
|
||||
| Tính năng | Mô tả |
|
||||
| --------------------------- | ------------------------------- |
|
||||
| **Hạn chế URL** | Chỉ cho phép URL chứng chỉ |
|
||||
| **Giới hạn kích thước tệp** | Tối đa 10MB mỗi yêu cầu |
|
||||
| **Giới hạn tốc độ** | 60 yêu cầu/IP/phút (cần KV) |
|
||||
| **Chặn IP riêng** | Chặn localhost và các IP nội bộ |
|
||||
|
||||
### Bật giới hạn tốc độ
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Build với URL proxy
|
||||
|
||||
```bash
|
||||
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
|
||||
```
|
||||
|
||||
Hoặc với 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 .
|
||||
```
|
||||
|
||||
> **Lưu ý:** Xem [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required) để biết cách thiết lập chữ ký HMAC.
|
||||
@@ -0,0 +1,180 @@
|
||||
# CORS Proxy để lấy chứng chỉ
|
||||
|
||||
Công cụ chữ ký số dùng một CORS proxy để lấy các chứng chỉ của bên cấp phát từ các Tổ chức cấp chứng chỉ (CA) bên ngoài. Điều này cần thiết vì nhiều máy chủ CA không đưa header CORS vào phản hồi, khiến trình duyệt không thể lấy trực tiếp.
|
||||
|
||||
Ngoài ra, nhiều máy chủ CA phục vụ chứng chỉ qua HTTP thường. Khi BentoPDF của bạn chạy trên HTTPS, trình duyệt sẽ chặn các yêu cầu HTTP này (chính sách nội dung hỗn hợp). CORS proxy giải quyết cả hai vấn đề bằng cách chuyển yêu cầu qua một endpoint HTTPS với các header phù hợp.
|
||||
|
||||
## Cách hoạt động
|
||||
|
||||
Khi ký PDF bằng chứng chỉ:
|
||||
|
||||
1. Thư viện `zgapdfsigner` cố gắng dựng một chuỗi chứng chỉ hoàn chỉnh
|
||||
2. Nó lấy các chứng chỉ của bên cấp phát từ các URL nhúng trong phần mở rộng AIA (Authority Information Access) của chứng chỉ bạn
|
||||
3. Các yêu cầu này được chuyển qua một CORS proxy thêm các header `Access-Control-Allow-Origin` cần thiết
|
||||
4. Proxy trả dữ liệu chứng chỉ về trình duyệt
|
||||
|
||||
## Tự lưu trữ CORS Proxy
|
||||
|
||||
Nếu bạn tự lưu trữ BentoPDF, bạn cần triển khai CORS proxy của riêng mình để chữ ký số hoạt động với các chứng chỉ cần lấy chuỗi chứng chỉ.
|
||||
|
||||
### Cách 1: Cloudflare Workers (khuyến nghị)
|
||||
|
||||
1. **Cài đặt Wrangler CLI**:
|
||||
|
||||
```bash
|
||||
npm install -g wrangler
|
||||
```
|
||||
|
||||
2. **Đăng nhập Cloudflare**:
|
||||
|
||||
```bash
|
||||
wrangler login
|
||||
```
|
||||
|
||||
3. **Clone BentoPDF và cập nhật các origin được phép**:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf/cloudflare
|
||||
```
|
||||
|
||||
Mở `cors-proxy-worker.js` và đổi mảng `ALLOWED_ORIGINS` thành tên miền của bạn:
|
||||
|
||||
```js
|
||||
const ALLOWED_ORIGINS = [
|
||||
'https://your-domain.com',
|
||||
'https://www.your-domain.com',
|
||||
];
|
||||
```
|
||||
|
||||
::: warning Quan trọng
|
||||
Nếu không thay đổi này, proxy sẽ từ chối mọi yêu cầu từ trang của bạn với lỗi **403 Forbidden**. Mặc định chỉ cho phép yêu cầu từ `bentopdf.com`.
|
||||
:::
|
||||
|
||||
4. **Triển khai proxy**:
|
||||
|
||||
```bash
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
Ghi lại URL worker của bạn (ví dụ `https://bentopdf-cors-proxy.your-subdomain.workers.dev`).
|
||||
|
||||
5. **Build lại BentoPDF với URL proxy**:
|
||||
|
||||
Nếu dùng 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 .
|
||||
```
|
||||
|
||||
Nếu build từ mã nguồn:
|
||||
|
||||
```bash
|
||||
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
|
||||
```
|
||||
|
||||
### Cách 2: Backend proxy tùy chỉnh
|
||||
|
||||
Bạn cũng có thể tự tạo endpoint proxy. Các yêu cầu là:
|
||||
|
||||
1. Chấp nhận yêu cầu GET với tham số truy vấn `url`
|
||||
2. Lấy URL đó từ máy chủ của bạn (phía máy chủ không bị hạn chế CORS)
|
||||
3. Trả phản hồi kèm các header sau:
|
||||
- `Access-Control-Allow-Origin: https://your-domain.com`
|
||||
- `Access-Control-Allow-Methods: GET, OPTIONS`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
|
||||
Ví dụ triển khai bằng 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' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Lưu ý bảo mật
|
||||
|
||||
Cloudflare Worker đi kèm có nhiều biện pháp bảo mật:
|
||||
|
||||
| Tính năng | Mô tả |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **Xác thực origin** | Chỉ cho phép yêu cầu từ các tên miền trong `ALLOWED_ORIGINS` |
|
||||
| **Hạn chế URL** | Chỉ cho phép URL chứng chỉ (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`, `/crl`) |
|
||||
| **Chặn IP riêng** | Chặn dải riêng IPv4/IPv6, link-local, loopback, IP dạng thập phân và metadata đám mây |
|
||||
| **An toàn Content-Type** | Chỉ trả về các MIME type chứng chỉ an toàn, chặn chèn content-type từ upstream |
|
||||
| **Giới hạn kích thước tệp** | Truyền phản hồi theo luồng với giới hạn 10MB, hủy giữa chừng nếu vượt quá |
|
||||
| **Giới hạn tốc độ** | 60 yêu cầu mỗi phút cho mỗi IP hoặc IPv6 /64 (cần KV, bỏ qua giới hạn khi KV lỗi) |
|
||||
| **Chữ ký HMAC** | Ký phía máy khách tùy chọn (ngăn chặn lạm dụng thông thường) |
|
||||
|
||||
### DNS rebinding (tự lưu trữ ngoài Cloudflare)
|
||||
|
||||
Worker từ chối mọi URL phân giải ra IP riêng hoặc nội bộ. Nó không thể dùng danh sách cho phép cố định các host, vì URL chuỗi chứng chỉ trỏ đến bất kỳ CA nào đã cấp chứng chỉ (FNMT, CA doanh nghiệp, v.v.), nên đích đến không biết trước. Điều đó để lại một khoảng hở rebinding nhỏ: người điều hành DNS cho tên miền của họ có thể đưa một IP công khai cho bước kiểm tra an toàn và một IP riêng cho yêu cầu thật ngay sau đó.
|
||||
|
||||
Trên Cloudflare điều này không khai thác được, vì Workers không thể truy cập IP riêng hay endpoint metadata `169.254.169.254`, nên rebind không dẫn đến đâu. Chỉ cần đừng cho worker thêm đường vào: đừng gắn Tunnel, Service hay binding mạng riêng (nó chỉ cần `RATE_LIMIT_KV`).
|
||||
|
||||
Nếu bạn chạy nó ngoài Cloudflare (workerd, Node và tương tự) bên trong VPC, runtime _có thể_ truy cập IP nội bộ. Hãy đặt tường lửa egress phía trước và chặn các dải riêng: `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`, và các dải IPv6 tương đương. Mã ứng dụng không thể thực thi điều đó khi bạn đã ra ngoài Cloudflare.
|
||||
|
||||
## Tắt Proxy
|
||||
|
||||
Nếu bạn không muốn dùng CORS proxy, hãy đặt biến môi trường thành chuỗi rỗng:
|
||||
|
||||
```
|
||||
VITE_CORS_PROXY_URL=
|
||||
```
|
||||
|
||||
**Lưu ý**: Khi không có proxy, việc ký bằng các chứng chỉ cần lấy chuỗi từ bên ngoài (như FNMT hoặc một số CA doanh nghiệp) sẽ thất bại với lỗi "Failed to fetch".
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### "Signing error: TypeError: Failed to fetch"
|
||||
|
||||
Điều này thường có nghĩa là:
|
||||
|
||||
1. **Chưa cấu hình CORS proxy**: Đặt `VITE_CORS_PROXY_URL` và build lại
|
||||
2. **Bị chặn nội dung hỗn hợp**: Trang của bạn dùng HTTPS nhưng URL bên cấp phát của chứng chỉ là HTTP. CORS proxy giải quyết việc này.
|
||||
3. **CORS proxy từ chối origin của bạn**: Kiểm tra tên miền của bạn có trong mảng `ALLOWED_ORIGINS` trong `cors-proxy-worker.js` không
|
||||
|
||||
### "403 Forbidden" từ proxy
|
||||
|
||||
Tên miền của bạn không có trong danh sách `ALLOWED_ORIGINS`. Sửa `cors-proxy-worker.js`:
|
||||
|
||||
```js
|
||||
const ALLOWED_ORIGINS = ['https://your-domain.com'];
|
||||
```
|
||||
|
||||
Sau đó triển khai lại: `npx wrangler deploy`
|
||||
|
||||
### Kiểm tra proxy
|
||||
|
||||
```bash
|
||||
curl -H "Origin: https://your-domain.com" \
|
||||
"https://your-proxy.workers.dev?url=http://www.cert.fnmt.es/certs/ACUSU.crt"
|
||||
```
|
||||
|
||||
### Chứng chỉ hoạt động không cần proxy
|
||||
|
||||
Một số chứng chỉ chứa đủ chuỗi trong tệp P12/PFX và không cần lấy từ bên ngoài:
|
||||
|
||||
- Chứng chỉ tự ký
|
||||
- Một số CA thương mại đóng gói sẵn các chứng chỉ trung gian
|
||||
- Chứng chỉ bạn tự ghép đủ chuỗi
|
||||
@@ -0,0 +1,622 @@
|
||||
# Triển khai với Docker / Podman
|
||||
|
||||
Cách dễ nhất để tự lưu trữ BentoPDF trong môi trường production.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Các header bắt buộc cho chuyển đổi tệp Office**
|
||||
>
|
||||
> Các công cụ dựa trên LibreOffice (chuyển đổi Word, Excel, PowerPoint) cần các header HTTP sau để hỗ trợ `SharedArrayBuffer`:
|
||||
>
|
||||
> - `Cross-Origin-Opener-Policy: same-origin`
|
||||
> - `Cross-Origin-Embedder-Policy: require-corp`
|
||||
>
|
||||
> Trang cũng phải được phục vụ từ một ngữ cảnh bảo mật (secure context). `http://localhost` dùng được để thử nghiệm cục bộ, nhưng `http://192.168.x.x` hoặc các IP LAN khác thường không đạt điều kiện, nên chuyển đổi Office qua HTTP thường sẽ thất bại dù các header đã có.
|
||||
>
|
||||
> Các container image chính thức đã bao gồm những header này. Nếu dùng reverse proxy (Traefik, Caddy, v.v.), hãy đảm bảo các header được giữ nguyên hoặc được thêm vào, và dùng HTTPS khi truy cập không qua loopback.
|
||||
|
||||
> [!TIP]
|
||||
> **Người dùng Podman:** Mọi lệnh `docker` đều chạy được với Podman bằng cách thay `docker` bằng `podman` và `docker-compose` bằng `podman-compose`.
|
||||
|
||||
## Bắt đầu nhanh
|
||||
|
||||
> [!TIP]
|
||||
> BentoPDF có hai bản build:
|
||||
>
|
||||
> - **Bản Tự lưu trữ** - `ghcr.io/alam00000/bentopdf-simple:latest`. Có mọi công cụ PDF, **không** có phần giao diện quảng bá (không hero, FAQ, lời chứng thực, chân trang). Dùng cho triển khai nội bộ/nhóm/tổ chức. Đây **không** phải bản "lite" bị giảm tính năng.
|
||||
> - **Bản Thương mại** - `ghcr.io/alam00000/bentopdf:latest`. Toàn bộ trang quảng bá, được chính bentopdf.com và các chủ sở hữu giấy phép thương mại dùng cho triển khai công khai.
|
||||
>
|
||||
> Nếu không chắc: hãy pull bản Tự lưu trữ.
|
||||
|
||||
```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
|
||||
|
||||
Tạo `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
|
||||
```
|
||||
|
||||
Chạy:
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose up -d
|
||||
|
||||
# Podman Compose
|
||||
podman-compose up -d
|
||||
```
|
||||
|
||||
## Tự build image
|
||||
|
||||
```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;"]
|
||||
```
|
||||
|
||||
Build và chạy:
|
||||
|
||||
```bash
|
||||
docker build -t bentopdf:custom .
|
||||
docker run -d -p 3000:8080 bentopdf:custom
|
||||
```
|
||||
|
||||
## Biến môi trường
|
||||
|
||||
| Biến | Mô tả | Mặc định |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `SIMPLE_MODE` | Build biến thể Tự lưu trữ (ẩn phần giao diện quảng bá của bentopdf.com: hero, FAQ, lời chứng thực, chân trang). Mọi công cụ PDF vẫn còn. | `false` |
|
||||
| `BASE_URL` | Triển khai vào thư mục con | `/` |
|
||||
| `VITE_WASM_PYMUPDF_URL` | URL module WASM PyMuPDF | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
|
||||
| `VITE_WASM_GS_URL` | URL module WASM Ghostscript | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` |
|
||||
| `VITE_WASM_CPDF_URL` | URL module WASM CoherentPDF | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` |
|
||||
| `VITE_TESSERACT_WORKER_URL` | URL tệp script worker OCR | _(trống; dùng CDN mặc định của Tesseract.js)_ |
|
||||
| `VITE_TESSERACT_CORE_URL` | Thư mục runtime lõi OCR | _(trống; dùng CDN mặc định của Tesseract.js)_ |
|
||||
| `VITE_TESSERACT_LANG_URL` | Thư mục traineddata OCR | _(trống; dùng CDN mặc định của Tesseract.js)_ |
|
||||
| `VITE_TESSERACT_AVAILABLE_LANGUAGES` | Các ngôn ngữ OCR hiển thị trong giao diện, phân tách bằng dấu phẩy | _(trống; hiển thị toàn bộ danh mục)_ |
|
||||
| `VITE_OCR_FONT_BASE_URL` | Thư mục phông chữ lớp văn bản OCR | _(trống; dùng URL phông Noto từ xa)_ |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Ngôn ngữ giao diện mặc định | `en` |
|
||||
| `VITE_BRAND_NAME` | Tên thương hiệu tùy chỉnh | `BentoPDF` |
|
||||
| `VITE_BRAND_LOGO` | Đường dẫn logo tương đối so với `public/` | `images/favicon-no-bg.svg` |
|
||||
| `VITE_FOOTER_TEXT` | Văn bản chân trang/bản quyền tùy chỉnh | `© 2026 BentoPDF. All rights reserved.` |
|
||||
| `DISABLE_TOOLS` | ID các công cụ cần ẩn, phân tách bằng dấu phẩy | _(trống; bật mọi công cụ)_ |
|
||||
| `DISABLE_GITHUB_STARS` | Bỏ qua việc lấy số sao GitHub khi tải trang và loại `api.github.com` khỏi CSP. Các bản build Simple Mode luôn bỏ qua. | `false` |
|
||||
| `SITE_URL` | URL công khai được ghi vào các liên kết canonical, hreflang và sitemap. Hãy giữ mặc định trừ khi bạn muốn công cụ tìm kiếm coi bản triển khai của bạn là một trang riêng. | `https://www.bentopdf.com` |
|
||||
| `ALLOW_INDEXING` | Với `SITE_URL` tùy chỉnh, mọi trang được đánh dấu `noindex` để bản của bạn không cạnh tranh với bentopdf.com trong kết quả tìm kiếm. Đặt `true` để cho phép công cụ tìm kiếm lập chỉ mục. | `false` |
|
||||
|
||||
URL các module WASM đã được cấu hình sẵn với CDN mặc định, nên mọi tính năng nâng cao hoạt động ngay. Hãy ghi đè các URL này cho môi trường air-gapped hoặc tự lưu trữ.
|
||||
|
||||
### Công cụ tìm kiếm
|
||||
|
||||
Theo mặc định, mọi trang đều chỉ công cụ tìm kiếm về bentopdf.com như bản gốc, nên bản triển khai của bạn không bị coi là trùng lặp. Bạn không cần thay đổi gì cho một bản riêng tư hoặc nội bộ.
|
||||
|
||||
- **Giữ bản của bạn hoàn toàn ngoài kết quả tìm kiếm:** chạy container với `-e ROBOTS_NOINDEX=true`. Chỉ cần khởi động lại, không cần build lại.
|
||||
- **Biến bản của bạn thành một trang riêng trong kết quả tìm kiếm:** build với `--build-arg SITE_URL=https://your-domain.example --build-arg ALLOW_INDEXING=true`. Nếu thiếu `ALLOW_INDEXING=true`, `SITE_URL` tùy chỉnh sẽ đánh dấu mọi trang là `noindex`.
|
||||
|
||||
### Content-Security-Policy
|
||||
|
||||
Image nginx đi kèm header `Content-Security-Policy` ở chế độ thực thi. Các chỉ thị `script-src`, `connect-src` và `font-src` của CSP được **tạo tại thời điểm build** từ các biến URL `VITE_*` ở trên: mọi host bạn truyền qua `--build-arg` đều tự động được thêm vào chính sách.
|
||||
|
||||
Do đó:
|
||||
|
||||
- Nếu bạn ghi đè `VITE_CORS_PROXY_URL` hoặc `VITE_WASM_*_URL` lúc build, CSP tự động cho phép các origin đó, không cần cấu hình thêm.
|
||||
- Với URL trên `cdn.jsdelivr.net`, `script-src` chỉ cho phép đúng thư mục gói có phiên bản, ví dụ `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/`, không phải toàn bộ CDN. jsDelivr phục vụ bất kỳ gói npm hoặc kho GitHub công khai nào, nên cho phép cả host sẽ để chúng chạy trên bản triển khai của bạn. URL trên mọi host khác được cho phép theo origin.
|
||||
- Nếu bạn cấu hình URL WASM tùy chỉnh lúc _chạy_ qua trang Cài đặt nâng cao trong ứng dụng, các origin đó **không** có trong CSP và trình duyệt sẽ chặn các yêu cầu tới chúng. Điều này cũng áp dụng cho phiên bản khác của cùng một gói trên `cdn.jsdelivr.net`, vì chỉ phiên bản đặt lúc build được cho phép. Cấu hình lúc chạy dành cho thử nghiệm; với URL tùy chỉnh lâu dài, hãy đặt build arg `VITE_*` tương ứng.
|
||||
- Các bản triển khai air-gapped ghi đè cả ba giá trị `VITE_WASM_*_URL` cũng sẽ bị loại các thư mục gói `cdn.jsdelivr.net` đó khỏi CSP (mỗi giá trị mặc định được thay thế, không phải nối thêm). Các mặc định Tesseract OCR trên `cdn.jsdelivr.net` vẫn nằm trong chính sách cho đến khi bạn cũng đặt ba giá trị `VITE_TESSERACT_*`. Tương tự, đặt `VITE_CORS_PROXY_URL` sẽ thay thế mặc định công khai `bentopdf-cors-proxy.bentopdf.workers.dev`.
|
||||
|
||||
CSP có `'unsafe-eval'` trong `script-src` vì runtime LibreOffice WASM (dùng cho các công cụ chuyển đổi Word/Excel/PowerPoint) và thư viện ảnh wasm-vips (dùng cho PDF sang TIFF) biên dịch mã điều phối nội bộ qua `new Function()`. Gỡ nó sẽ làm hỏng các công cụ này. Nếu bạn build ở `SIMPLE_MODE` (không có LibreOffice) và không cần PDF sang TIFF, bạn có thể tự sửa tệp `security-headers.conf` được tạo ra để bỏ `'unsafe-eval'` cho chính sách chặt hơn.
|
||||
|
||||
Với OCR, hãy để trống các biến `VITE_TESSERACT_*` để dùng tài nguyên trực tuyến mặc định, hoặc đặt cả ba cùng lúc cho OCR tự lưu trữ/ngoại tuyến. Ghi đè OCR một phần sẽ bị từ chối vì worker, runtime lõi và thư mục traineddata phải khớp nhau. Để có đầu ra PDF có thể tìm kiếm hoàn toàn ngoại tuyến, hãy đặt thêm `VITE_OCR_FONT_BASE_URL` để phông chữ lớp văn bản OCR được tải từ máy chủ nội bộ của bạn thay vì URL phông Noto công khai.
|
||||
|
||||
`VITE_DEFAULT_LANGUAGE` đặt ngôn ngữ giao diện cho khách truy cập lần đầu. Giá trị được hỗ trợ: `en`, `ar`, `be`, `fr`, `de`, `es`, `zh`, `zh-TW`, `vi`, `tr`, `id`, `it`, `pt`, `nl`, `da`. Người dùng vẫn có thể đổi ngôn ngữ, giá trị này chỉ đổi mặc định.
|
||||
|
||||
Ví dụ:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Thương hiệu tùy chỉnh
|
||||
|
||||
Thay logo, tên và văn bản chân trang BentoPDF mặc định bằng của riêng bạn. Đặt tệp logo vào thư mục `public/` (hoặc dùng một ảnh có sẵn), sau đó truyền các biến thương hiệu lúc build:
|
||||
|
||||
```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 .
|
||||
```
|
||||
|
||||
Thương hiệu hoạt động ở cả chế độ đầy đủ và Simple Mode, và có thể kết hợp với mọi tùy chọn build khác.
|
||||
|
||||
### Tắt một số công cụ
|
||||
|
||||
Ẩn công cụ khỏi giao diện để đáp ứng yêu cầu tuân thủ hoặc bảo mật. Các công cụ bị tắt sẽ bị gỡ khỏi trang chủ, kết quả tìm kiếm, phím tắt và trình tạo quy trình. Truy cập trực tiếp bằng URL sẽ hiển thị trang "công cụ không khả dụng".
|
||||
|
||||
ID công cụ là URL của trang bỏ `.html`. Ví dụ, nếu công cụ nằm ở `bentopdf.com/edit-pdf.html`, ID là `edit-pdf`.
|
||||
|
||||
#### Tìm ID công cụ
|
||||
|
||||
Cách dễ nhất: mở bất kỳ công cụ nào trong BentoPDF và nhìn vào URL. Phần cuối của đường dẫn (bỏ `.html`) chính là ID công cụ.
|
||||
|
||||
<details>
|
||||
<summary>Danh sách đầy đủ ID công cụ</summary>
|
||||
|
||||
**Chỉnh sửa & Chú thích:** `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`
|
||||
|
||||
**Chuyển sang 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`
|
||||
|
||||
**Chuyển từ 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`
|
||||
|
||||
**Sắp xếp & Quản lý:** `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`
|
||||
|
||||
**Tối ưu hóa & Sửa chữa:** `compress-pdf`, `pdf-to-pdfa`, `fix-page-size`, `linearize-pdf`, `remove-restrictions`, `repair-pdf`, `rasterize-pdf`, `deskew-pdf`, `font-to-outline`
|
||||
|
||||
**Bảo mật:** `encrypt-pdf`, `sanitize-pdf`, `decrypt-pdf`, `flatten-pdf`, `remove-metadata`, `change-permissions`, `digital-sign-pdf`, `validate-signature-pdf`, `timestamp-pdf`
|
||||
|
||||
</details>
|
||||
|
||||
#### Cách 1: Lúc build (build arg của Docker)
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--build-arg DISABLE_TOOLS="edit-pdf,sign-pdf,encrypt-pdf" \
|
||||
-t bentopdf .
|
||||
```
|
||||
|
||||
Cách này ghi danh sách công cụ bị tắt vào bundle JavaScript. Cần build lại để thay đổi.
|
||||
|
||||
#### Cách 2: Lúc chạy (config.json)
|
||||
|
||||
Gắn một tệp `config.json` vào thư mục được phục vụ, không cần build lại:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Hoặc với 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
|
||||
```
|
||||
|
||||
Có thể kết hợp cả hai cách, các danh sách sẽ được gộp. Nếu một công cụ xuất hiện ở một trong hai, nó sẽ bị tắt.
|
||||
|
||||
#### Tắt tính năng của trình chỉnh sửa
|
||||
|
||||
Bạn cũng có thể tắt các tính năng cụ thể trong Trình chỉnh sửa PDF (ví dụ che thông tin, chú thích, biểu mẫu) mà không cần tắt cả công cụ chỉnh sửa. Thêm `editorDisabledCategories` vào `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"editorDisabledCategories": ["redaction", "annotation-stamp"]
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Danh sách đầy đủ danh mục của trình chỉnh sửa</summary>
|
||||
|
||||
**Zoom:** `zoom`, `zoom-in`, `zoom-out`, `zoom-fit-page`, `zoom-fit-width`, `zoom-marquee`, `zoom-level`
|
||||
|
||||
**Chú thích:** `annotation`, `annotation-markup`, `annotation-highlight`, `annotation-underline`, `annotation-strikeout`, `annotation-squiggly`, `annotation-ink`, `annotation-text`, `annotation-stamp`
|
||||
|
||||
**Hình dạng:** `annotation-shape`, `annotation-rectangle`, `annotation-circle`, `annotation-line`, `annotation-arrow`, `annotation-polygon`, `annotation-polyline`
|
||||
|
||||
**Biểu mẫu:** `form`, `form-textfield`, `form-checkbox`, `form-radio`, `form-select`, `form-listbox`, `form-fill-mode`
|
||||
|
||||
**Che thông tin:** `redaction`, `redaction-area`, `redaction-text`, `redaction-apply`, `redaction-clear`
|
||||
|
||||
**Tài liệu:** `document`, `document-open`, `document-close`, `document-print`, `document-capture`, `document-export`, `document-fullscreen`, `document-protect`
|
||||
|
||||
**Trang:** `page`, `spread`, `rotate`, `scroll`, `navigation`
|
||||
|
||||
**Bảng điều khiển:** `panel`, `panel-sidebar`, `panel-search`, `panel-comment`
|
||||
|
||||
**Công cụ:** `tools`, `pan`, `pointer`, `capture`
|
||||
|
||||
**Lựa chọn:** `selection`, `selection-copy`
|
||||
|
||||
**Lịch sử:** `history`, `history-undo`, `history-redo`
|
||||
|
||||
</details>
|
||||
|
||||
Các danh mục có cấu trúc phân cấp: tắt một mục cha (ví dụ `annotation`) sẽ tắt toàn bộ mục con của nó.
|
||||
|
||||
### Tắt bộ đếm sao GitHub
|
||||
|
||||
Thanh điều hướng hiển thị số sao GitHub trực tiếp, được lấy từ `api.github.com` khi tải trang. Đây là yêu cầu bên ngoài duy nhất không phục vụ một tính năng PDF. Hãy tắt nó lúc build để gỡ mã lấy dữ liệu khỏi bundle và loại `api.github.com` khỏi Content-Security-Policy:
|
||||
|
||||
```bash
|
||||
docker build --build-arg DISABLE_GITHUB_STARS=true -t bentopdf .
|
||||
```
|
||||
|
||||
Các bản build Simple Mode (`bentopdf-simple`) luôn bỏ qua bộ đếm sao, cờ này chỉ cần cho bản Thương mại hoặc các bản build đầy đủ tùy chỉnh.
|
||||
|
||||
### URL WASM tùy chỉnh (Air-Gapped / Tự lưu trữ)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> URL WASM được ghi vào JavaScript lúc **build**. Các tệp WASM được **trình duyệt của người dùng** tải về lúc chạy, Docker không tải chúng trong quá trình build. Với mạng air-gapped, bạn phải lưu trữ các tệp WASM trên một máy chủ nội bộ mà trình duyệt truy cập được.
|
||||
|
||||
**Quy trình air-gapped đầy đủ:**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Dùng các mã được in bởi `bash scripts/prepare-airgap.sh --list-ocr-languages`, hoặc tìm theo tên bằng `bash scripts/prepare-airgap.sh --search-ocr-language <term>`, cho `--ocr-languages`. Khi build với một tập con OCR giới hạn, hãy truyền cùng các mã đó cho `VITE_TESSERACT_AVAILABLE_LANGUAGES` để ứng dụng chỉ hiển thị các ngôn ngữ đã đóng gói. Để có đầu ra OCR ngoại tuyến đầy đủ, hãy lưu trữ thêm thư mục `ocr-fonts/` đi kèm và trỏ `VITE_OCR_FONT_BASE_URL` tới nó.
|
||||
|
||||
Đặt một biến thành chuỗi rỗng để tắt module đó (người dùng phải tự cấu hình qua Cài đặt nâng cao).
|
||||
|
||||
## Cổng tùy chỉnh
|
||||
|
||||
Theo mặc định, BentoPDF lắng nghe ở cổng `8080` bên trong container. Để đổi, hãy đặt biến môi trường `PORT` lúc chạy:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
|
||||
```
|
||||
|
||||
| Biến | Mô tả | Mặc định |
|
||||
| ------ | ------------------------------------ | -------- |
|
||||
| `PORT` | Cổng lắng nghe Nginx trong container | `8080` |
|
||||
|
||||
Cách này hoạt động với cả Dockerfile tiêu chuẩn và nonroot.
|
||||
|
||||
## User ID tùy chỉnh (PUID/PGID)
|
||||
|
||||
Với các môi trường yêu cầu chạy bằng một người dùng không phải root cụ thể (thiết bị NAS, Kubernetes có security context, chính sách của tổ chức), BentoPDF cung cấp một Dockerfile riêng hỗ trợ PUID/PGID kiểu LSIO.
|
||||
|
||||
### Build và chạy
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Biến môi trường
|
||||
|
||||
| Biến | Mô tả | Mặc định |
|
||||
| -------------- | -------------------- | -------- |
|
||||
| `PUID` | User ID để chạy | `1000` |
|
||||
| `PGID` | Group ID để chạy | `1000` |
|
||||
| `PORT` | Cổng lắng nghe Nginx | `8080` |
|
||||
| `DISABLE_IPV6` | Tắt listener 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
|
||||
```
|
||||
|
||||
### Cách hoạt động
|
||||
|
||||
Container khởi động bằng root, tạo một người dùng với PUID/PGID đã chỉ định, điều chỉnh quyền sở hữu trên mọi thư mục có thể ghi, rồi hạ quyền bằng `su-exec`. Tiến trình nginx chạy hoàn toàn dưới người dùng bạn chỉ định.
|
||||
|
||||
> [!NOTE]
|
||||
> `Dockerfile` tiêu chuẩn dùng `nginx-unprivileged` (UID 101) và được khuyến nghị cho hầu hết triển khai. Chỉ dùng `Dockerfile.nonroot` khi bạn cần một UID/GID cụ thể.
|
||||
|
||||
> [!WARNING]
|
||||
> PUID/PGID không được là `0` (root). Entrypoint kiểm tra đầu vào và sẽ thoát với lỗi nếu giá trị không hợp lệ.
|
||||
|
||||
## Với Traefik (Reverse Proxy)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Với Caddy (Reverse Proxy)
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
## Giới hạn tài nguyên
|
||||
|
||||
```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 (tích hợp Systemd)
|
||||
|
||||
[Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) cho phép bạn chạy container Podman như các dịch vụ systemd. Rất phù hợp cho triển khai production trên hệ thống Linux.
|
||||
|
||||
### Thiết lập Quadlet cơ bản
|
||||
|
||||
Tạo tệp unit container tại `~/.config/containers/systemd/bentopdf.container` (người dùng) hoặc `/etc/containers/systemd/bentopdf.container` (hệ thống):
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Bật và khởi động
|
||||
|
||||
```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]
|
||||
> Với triển khai toàn hệ thống, dùng `systemctl` không kèm cờ `--user` và đặt tệp trong `/etc/containers/systemd/`.
|
||||
|
||||
### Quadlet với kiểm tra sức khỏe
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Tự động cập nhật với Quadlet
|
||||
|
||||
Podman có thể tự động cập nhật container khi có image mới:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Cấu hình mạng Quadlet
|
||||
|
||||
Để cấu hình mạng tùy chỉnh, tạo tệp mạng `bentopdf.network`:
|
||||
|
||||
```ini
|
||||
[Network]
|
||||
Subnet=10.89.0.0/24
|
||||
Gateway=10.89.0.1
|
||||
```
|
||||
|
||||
Sau đó tham chiếu nó trong tệp container của bạn:
|
||||
|
||||
```ini
|
||||
[Container]
|
||||
Image=ghcr.io/alam00000/bentopdf-simple:latest
|
||||
ContainerName=bentopdf
|
||||
PublishPort=3000:8080
|
||||
Network=bentopdf.network
|
||||
```
|
||||
|
||||
## Cập nhật
|
||||
|
||||
```bash
|
||||
# Pull latest image
|
||||
docker compose pull
|
||||
|
||||
# Recreate container
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -0,0 +1,266 @@
|
||||
# Triển khai lên Hostinger
|
||||
|
||||
[Hostinger](https://hostinger.com) là nhà cung cấp shared hosting phổ biến. Hướng dẫn này nói về cách triển khai BentoPDF lên shared hosting của Hostinger.
|
||||
|
||||
## Yêu cầu
|
||||
|
||||
- Gói hosting Hostinger có quyền truy cập File Manager
|
||||
- Đã cài Node.js trên máy để build
|
||||
|
||||
## Bước 1: Build dự án
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Các tệp đã build sẽ nằm trong thư mục `dist`.
|
||||
|
||||
## Bước 2: Tải lên Hostinger
|
||||
|
||||
### Triển khai trên tên miền gốc
|
||||
|
||||
1. Đăng nhập vào tài khoản Hostinger của bạn
|
||||
2. Vào **File Manager** → **public_html**
|
||||
3. **Xóa** mọi tệp hiện có (sao lưu nếu cần)
|
||||
4. **Tải lên** toàn bộ nội dung thư mục `dist` trên máy của bạn vào `public_html`
|
||||
|
||||
### Triển khai trong thư mục con
|
||||
|
||||
Nếu triển khai vào thư mục con (ví dụ `yourdomain.com/pdf-tools/`):
|
||||
|
||||
1. Build với base URL đúng:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf-tools/ npm run build
|
||||
```
|
||||
|
||||
2. Tạo thư mục trong Hostinger:
|
||||
- Vào **File Manager** → **public_html**
|
||||
- Tạo thư mục mới: `pdf-tools`
|
||||
3. Tải toàn bộ nội dung `dist` lên `public_html/pdf-tools/`
|
||||
|
||||
## Bước 3: Tạo tệp .htaccess
|
||||
|
||||
Tạo tệp `.htaccess` ở thư mục gốc của bản triển khai (`public_html` hoặc `public_html/pdf-tools/`):
|
||||
|
||||
::: warning Quan trọng
|
||||
Với triển khai trong thư mục con, đổi `RewriteBase /` thành `RewriteBase /pdf-tools/` (hoặc tên thư mục của bạn).
|
||||
:::
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Ví dụ .htaccess cho thư mục con
|
||||
|
||||
Với `yourdomain.com/pdf-tools/`, hãy cập nhật các dòng sau:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### Tệp WASM không tải được
|
||||
|
||||
Đảm bảo các MIME type được đặt đúng trong `.htaccess`:
|
||||
|
||||
```apache
|
||||
AddType application/wasm .wasm
|
||||
```
|
||||
|
||||
### Công cụ LibreOffice không hoạt động
|
||||
|
||||
Các header bảo mật rất quan trọng cho SharedArrayBuffer:
|
||||
|
||||
```apache
|
||||
Header always set Cross-Origin-Opener-Policy "same-origin"
|
||||
Header always set Cross-Origin-Embedder-Policy "require-corp"
|
||||
```
|
||||
|
||||
Nếu các header không được áp dụng, hãy liên hệ bộ phận hỗ trợ của Hostinger để bật `mod_headers`.
|
||||
|
||||
### Lỗi 404 khi tải lại trang
|
||||
|
||||
Đảm bảo quy tắc đuôi `.html` và các tuyến ngôn ngữ được cấu hình đúng. BentoPDF dùng các tệp HTML tĩnh, không dùng định tuyến 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]
|
||||
```
|
||||
|
||||
### Giới hạn tải tệp lên
|
||||
|
||||
Hostinger có thể giới hạn kích thước tệp. Hãy tải các tệp WASM lên riêng nếu tải hàng loạt thất bại.
|
||||
@@ -0,0 +1,538 @@
|
||||
# Hướng dẫn tự lưu trữ
|
||||
|
||||
BentoPDF có thể được tự lưu trữ trên hạ tầng của riêng bạn. Hướng dẫn này trình bày nhiều tùy chọn triển khai.
|
||||
|
||||
## Bắt đầu nhanh với Docker / Podman
|
||||
|
||||
Cách nhanh nhất để tự lưu trữ BentoPDF:
|
||||
|
||||
> [!TIP]
|
||||
> BentoPDF có hai bản dựng:
|
||||
>
|
||||
> - **Bản dựng Self-Hosted**: `ghcr.io/alam00000/bentopdf-simple:latest`. Có mọi công cụ PDF, **không** có phần giao diện tiếp thị (không có hero, FAQ, lời chứng thực, chân trang). Dùng bản này cho các triển khai nội bộ, theo nhóm hoặc tổ chức. Đây **không phải** là phiên bản "lite" bị giảm tính năng.
|
||||
> - **Bản dựng Commercial**: `ghcr.io/alam00000/bentopdf:latest`. Toàn bộ trang tiếp thị, được chính bentopdf.com và các chủ sở hữu giấy phép thương mại chạy triển khai công khai sử dụng.
|
||||
>
|
||||
> Nếu còn phân vân: hãy dùng bản dựng Self-Hosted.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Chuyển đổi tệp Office cần `SharedArrayBuffer`, nghĩa là ứng dụng phải được cô lập cross-origin và phục vụ từ một ngữ cảnh bảo mật. Image chính thức đã gửi sẵn các header COOP/COEP cần thiết, nhưng trình duyệt vẫn tắt `SharedArrayBuffer` trên các origin HTTP mạng nội bộ như `http://192.168.x.x`.
|
||||
>
|
||||
> Chỉ dùng `http://localhost` để thử nghiệm trên cùng một thiết bị. Nếu người dùng truy cập BentoPDF qua IP hoặc tên máy trong mạng LAN, hãy dùng 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
|
||||
```
|
||||
|
||||
Hoặc với 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)
|
||||
|
||||
Chạy BentoPDF như một dịch vụ systemd. Tạo `~/.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
|
||||
```
|
||||
|
||||
Xem [hướng dẫn triển khai Docker](/vi/self-hosting/docker) để biết đầy đủ tài liệu về Quadlet.
|
||||
|
||||
## Dựng từ mã nguồn
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Tùy chọn cấu hình
|
||||
|
||||
### Bản dựng Self-Hosted (Simple Mode)
|
||||
|
||||
Bản dựng Self-Hosted (image `bentopdf-simple`, còn gọi là Simple Mode) **giống hệt về chức năng** với bản dựng Commercial: mọi công cụ PDF đều có và hoạt động như nhau. Nó chỉ ẩn phần giao diện tiếp thị vốn chỉ phù hợp với chính bentopdf.com hoặc một triển khai công khai thương mại. **Đây không phải là phiên bản bị giảm tính năng hay "lite".**
|
||||
|
||||
**Những gì bản dựng Self-Hosted ẩn đi** (chỉ là phần hiển thị, không có tính năng PDF nào bị gỡ bỏ):
|
||||
|
||||
- Thanh điều hướng, phần hero, phần tính năng, FAQ, lời chứng thực, chân trang
|
||||
- Đổi tiêu đề trang thành "Công cụ PDF"
|
||||
|
||||
**Những gì bản dựng Self-Hosted giữ lại** (mọi thứ thực sự xử lý PDF):
|
||||
|
||||
- Mọi công cụ PDF (gộp, tách, chỉnh sửa, ký, OCR, chuyển đổi Office, v.v.)
|
||||
- Hỗ trợ thương hiệu tùy chỉnh, toàn bộ cấu hình lúc dựng và lúc chạy
|
||||
|
||||
Bản dựng Commercial (`ghcr.io/alam00000/bentopdf:latest`) là bản vận hành chính bentopdf.com và được các chủ sở hữu giấy phép thương mại chạy triển khai công khai sử dụng. Nó bổ sung hero, FAQ, lời chứng thực và chân trang, những thứ không phù hợp với một công cụ nội bộ.
|
||||
|
||||
Nếu bạn tự lưu trữ BentoPDF cho nhóm, tổ chức hoặc làm công cụ nội bộ, hãy dùng bản dựng Self-Hosted:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Xem [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md) để biết chi tiết.
|
||||
|
||||
### Bản dựng Commercial (triển khai công khai với thương hiệu riêng)
|
||||
|
||||
Bản dựng Commercial (image `bentopdf`, không có hậu tố `-simple`) là bản vận hành chính bentopdf.com. Nó gồm toàn bộ trang tiếp thị (hero, tính năng, FAQ, lời chứng thực, chân trang) cùng mọi công cụ PDF. Hãy dùng bản dựng này khi bạn muốn **triển khai BentoPDF thành dịch vụ PDF công khai dưới thương hiệu của riêng bạn**, ví dụ:
|
||||
|
||||
- Bạn chạy BentoPDF như một SaaS cho người dùng cuối trên tên miền của riêng bạn
|
||||
- Bạn muốn có trải nghiệm trang chủ (phần tiếp thị cùng công cụ), không chỉ giao diện công cụ trơn
|
||||
- Bạn là chủ sở hữu giấy phép thương mại và nhúng BentoPDF vào một sản phẩm thương mại
|
||||
|
||||
**Chạy nguyên bản** (thương hiệu BentoPDF, hữu ích để xem trước bản dựng):
|
||||
|
||||
```bash
|
||||
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
|
||||
```
|
||||
|
||||
**Dựng với thương hiệu riêng** (quy trình thương mại điển hình):
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Mọi tùy chọn lúc dựng khác (`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, ghi đè URL WASM, v.v.) hoạt động giống như trên bản dựng Self-Hosted.
|
||||
|
||||
::: warning Giấy phép
|
||||
Chạy bản dựng Commercial được phép theo một trong hai tùy chọn giấy phép của BentoPDF:
|
||||
|
||||
- **AGPL-3.0** (miễn phí): được phép nếu triển khai của bạn công bố toàn bộ mã nguồn theo AGPL, bao gồm mọi chỉnh sửa thương hiệu và logic nghiệp vụ liên quan.
|
||||
- **Giấy phép thương mại** ($79 trọn đời): bắt buộc cho các triển khai mã nguồn đóng / độc quyền, nơi bạn không mở mã nguồn của bản fork thương hiệu hay mã nghiệp vụ.
|
||||
|
||||
Xem [trang Giấy phép](https://www.bentopdf.com/licensing) để so sánh đầy đủ. Các mô-đun WASM theo giấy phép AGPL (PyMuPDF, Ghostscript, CoherentPDF) được tải từ CDN lúc chạy, nên chúng không nằm trong image của bạn và không thay đổi tình trạng giấy phép của bạn.
|
||||
:::
|
||||
|
||||
### Base URL
|
||||
|
||||
Triển khai vào một thư mục con:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf-tools/ npm run build
|
||||
```
|
||||
|
||||
### Thương hiệu tùy chỉnh
|
||||
|
||||
Thay logo, tên và văn bản chân trang mặc định của BentoPDF bằng của bạn ngay lúc dựng:
|
||||
|
||||
| Variable | Mô tả | Mặc định |
|
||||
| ------------------ | -------------------------------------------- | --------------------------------------- |
|
||||
| `VITE_BRAND_NAME` | Tên thương hiệu hiển thị ở đầu và chân trang | `BentoPDF` |
|
||||
| `VITE_BRAND_LOGO` | Đường dẫn logo, tính từ `public/` | `images/favicon-no-bg.svg` |
|
||||
| `VITE_FOOTER_TEXT` | Văn bản chân trang / bản quyền tùy chỉnh | `© 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
|
||||
```
|
||||
|
||||
Hoặc qua 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 .
|
||||
```
|
||||
|
||||
Thương hiệu hoạt động ở cả chế độ đầy đủ lẫn Simple Mode, và có thể kết hợp với mọi tùy chọn lúc dựng khác (`BASE_URL`, `SIMPLE_MODE`, `VITE_DEFAULT_LANGUAGE`).
|
||||
|
||||
### Tắt công cụ cụ thể
|
||||
|
||||
Ẩn từng công cụ vì lý do tuân thủ hoặc bảo mật. Công cụ bị tắt sẽ bị loại khỏi trang chủ, tìm kiếm, phím tắt, trình tạo quy trình và truy cập trực tiếp bằng URL.
|
||||
|
||||
ID công cụ là URL của trang bỏ đi `.html`. Hãy mở bất kỳ công cụ nào và xem URL (ví dụ: `edit-pdf`, `sign-pdf`, `encrypt-pdf`).
|
||||
|
||||
**Lúc dựng** (được đưa thẳng vào bundle):
|
||||
|
||||
```bash
|
||||
DISABLE_TOOLS="edit-pdf,sign-pdf" npm run build
|
||||
```
|
||||
|
||||
**Lúc chạy** (không cần dựng lại, gắn một tệp `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
|
||||
```
|
||||
|
||||
Có thể kết hợp cả hai cách, các danh sách sẽ được gộp lại.
|
||||
|
||||
Bạn cũng có thể tắt một số tính năng bên trong Trình chỉnh sửa PDF (ví dụ: biên tập che thông tin) mà không cần tắt cả công cụ, bằng cách thêm `editorDisabledCategories` vào `config.json`. Xem [hướng dẫn Docker](/vi/self-hosting/docker#tat-tinh-nang-cua-trinh-chinh-sua) để biết danh sách đầy đủ các nhóm.
|
||||
|
||||
## Hướng dẫn triển khai
|
||||
|
||||
Chọn nền tảng của bạn:
|
||||
|
||||
- [Vercel](/vi/self-hosting/vercel)
|
||||
- [Netlify](/vi/self-hosting/netlify)
|
||||
- [Cloudflare Pages](/vi/self-hosting/cloudflare)
|
||||
- [AWS S3 + CloudFront](/vi/self-hosting/aws)
|
||||
- [Hostinger](/vi/self-hosting/hostinger)
|
||||
- [Nginx](/vi/self-hosting/nginx)
|
||||
- [Apache](/vi/self-hosting/apache)
|
||||
- [Docker](/vi/self-hosting/docker)
|
||||
- [Kubernetes](/vi/self-hosting/kubernetes)
|
||||
- [CORS Proxy](/vi/self-hosting/cors-proxy) - Bắt buộc cho chữ ký số
|
||||
|
||||
## Các sự cố thường gặp
|
||||
|
||||
### Chuyển Word / ODT / Excel / PowerPoint sang PDF bị treo (SharedArrayBuffer không khả dụng)
|
||||
|
||||
**Triệu chứng**: Các chuyển đổi tài liệu dựa trên LibreOffice (Word, ODT, Excel, PowerPoint sang PDF) bị treo ở khoảng 55% hoặc không thể bắt đầu. Bảng điều khiển trình duyệt có thể hiển thị:
|
||||
|
||||
> `ReferenceError: SharedArrayBuffer is not defined`
|
||||
|
||||
...hoặc `window.crossOriginIsolated` trả về `false`, hoặc quá trình biên dịch WASM báo `expected magic word 00 61 73 6d, found 1f 8b 08 08`.
|
||||
|
||||
**Nguyên nhân**: LibreOffice WASM cần `SharedArrayBuffer`, thứ mà trình duyệt chỉ bật khi trang được **cô lập cross-origin** VÀ được phục vụ từ một **ngữ cảnh bảo mật**. Điều đó nghĩa là cần đảm bảo hai điều:
|
||||
|
||||
1. Mọi phản hồi đều có cả hai header:
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
2. Trang được phục vụ từ `https://...` hoặc `http://localhost`. HTTP thường trên IP mạng LAN (ví dụ `http://192.168.x.x`) KHÔNG được coi là bảo mật: trình duyệt tắt `SharedArrayBuffer` ở đó.
|
||||
|
||||
Việc lệch `00 61 73 6d / 1f 8b 08 08` là một triệu chứng phụ riêng: các tệp `.wasm.gz` / `.data.gz` đã nén sẵn thiếu header phản hồi `Content-Encoding: gzip`, nên trình duyệt nhận dữ liệu gzip thô thay vì WASM đã giải nén.
|
||||
|
||||
**Cách khắc phục**: xem hướng dẫn triển khai riêng của nền tảng bạn để biết cấu hình chính xác:
|
||||
|
||||
- [Nginx →](/vi/self-hosting/nginx#chuyen-word-odt-excel-sang-pdf-khong-hoat-đong)
|
||||
- [Apache →](/vi/self-hosting/apache#chuyen-word-odt-excel-sang-pdf-khong-hoat-đong)
|
||||
- [AWS S3 + CloudFront →](/vi/self-hosting/aws#buoc-3b-chinh-sach-header-phan-hoi-bat-buoc-cho-libreoffice-wasm)
|
||||
- [Cloudflare Pages →](/vi/self-hosting/cloudflare#tep-cau-hinh) (tệp `_headers`)
|
||||
- [Netlify →](/vi/self-hosting/netlify#chuyen-word-odt-excel-sang-pdf-bi-đung-o-55) (`netlify.toml`)
|
||||
- [Vercel →](/vi/self-hosting/vercel#chuyen-word-odt-excel-sang-pdf-khong-hoat-đong) (`vercel.json`)
|
||||
- [Hostinger →](/vi/self-hosting/hostinger#cong-cu-libreoffice-khong-hoat-đong) (`.htaccess`)
|
||||
- [Kubernetes →](/vi/self-hosting/kubernetes#đam-bao-cac-header-sharedarraybuffer-van-hoat-đong-ingress-gateway)
|
||||
- **Docker**: được xử lý tự động bởi cấu hình nginx đi kèm, không cần làm gì thêm.
|
||||
|
||||
**Kiểm tra**: mở Console của DevTools trên bất kỳ trang BentoPDF nào và chạy:
|
||||
|
||||
```js
|
||||
console.log(window.crossOriginIsolated); // should be true
|
||||
console.log(typeof SharedArrayBuffer); // should be "function"
|
||||
```
|
||||
|
||||
Nếu trang dùng HTTPS hoặc `http://localhost` VÀ cả hai header COEP/COOP đều có trên mọi phản hồi, cả hai phép kiểm tra sẽ đạt. Nếu bạn đang dùng `http://192.168.x.x` hoặc một origin HTTP không phải loopback khác, hãy dùng HTTPS: không có cách khắc phục nào chỉ bằng header.
|
||||
|
||||
### Tệp `.mjs` được phục vụ với `application/octet-stream`
|
||||
|
||||
**Triệu chứng**: Ký PDF / Form Filler / một số công cụ khác hiển thị trình xem trống hoặc không tải được. Bảng điều khiển trình duyệt báo:
|
||||
|
||||
> `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.`
|
||||
|
||||
**Nguyên nhân**: Máy chủ web hoặc reverse proxy của bạn không có ánh xạ kiểu MIME cho tệp `.mjs` (trình xem PDF đi kèm dùng module ES với phần mở rộng này). Nhiều cấu hình máy chủ mặc định dùng `application/octet-stream` cho các phần mở rộng không nhận ra, và trình duyệt từ chối thực thi chúng như module ES.
|
||||
|
||||
**Cách khắc phục**: xem hướng dẫn triển khai riêng của nền tảng bạn để biết đoạn cấu hình chính xác:
|
||||
|
||||
- [Nginx →](/vi/self-hosting/nginx#ky-pdf-hoac-form-filler-hien-thi-trinh-xem-trong-loi-mime-mjs)
|
||||
- [Apache →](/vi/self-hosting/apache#ky-pdf-hoac-form-filler-hien-thi-trinh-xem-trong-loi-mime-mjs)
|
||||
- [AWS S3 + CloudFront →](/vi/self-hosting/aws#buoc-2-build-va-tai-len) (xem `aws s3 cp ... --include "*.mjs"`)
|
||||
- [Kubernetes →](/vi/self-hosting/kubernetes#loi-kieu-mime-cua-mjs-ky-pdf-iframe-form-filler-bi-trong)
|
||||
- **Docker, Vercel, Netlify, Cloudflare Pages, Hostinger**: được xử lý tự động, không cần làm gì thêm.
|
||||
|
||||
**Kiểm tra**: mở DevTools → tab Network, tìm yêu cầu `.mjs` bị lỗi, xem header phản hồi `Content-Type`. Giá trị phải là `application/javascript`. Nếu vẫn là `application/octet-stream`, có thể một reverse proxy hoặc CDN bên ngoài đang tự đoán lại kiểu: hãy kiểm tra từng lớp trong chuỗi phục vụ của bạn.
|
||||
|
||||
## Cấu hình WASM (thành phần AGPL)
|
||||
|
||||
BentoPDF **không đóng gói** các thư viện xử lý theo giấy phép AGPL trong mã nguồn của mình, nhưng **cấu hình sẵn các URL CDN** để mọi tính năng hoạt động ngay, không cần thiết lập thủ công.
|
||||
|
||||
::: tip Không cần cấu hình theo mặc định
|
||||
Từ v2.0.0, các mô-đun WASM được cấu hình sẵn để tải từ CDN jsDelivr thông qua biến môi trường. Mọi tính năng nâng cao hoạt động ngay mà người dùng không cần cấu hình gì.
|
||||
:::
|
||||
|
||||
| Thành phần | Giấy phép | Tính năng |
|
||||
| --------------- | --------- | ------------------------------------------------------------- |
|
||||
| **PyMuPDF** | AGPL-3.0 | Chuyển đổi EPUB/MOBI/FB2/XPS, trích xuất ảnh, trích xuất bảng |
|
||||
| **Ghostscript** | AGPL-3.0 | Chuyển đổi PDF/A, nén, chỉnh nghiêng, rasterize |
|
||||
| **CoherentPDF** | AGPL-3.0 | Mục lục, tệp đính kèm, gộp PDF kèm dấu trang |
|
||||
|
||||
### Biến môi trường mặc định
|
||||
|
||||
Các biến này được đặt trong `.env.production` và đưa thẳng vào bản dựng:
|
||||
|
||||
```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=
|
||||
```
|
||||
|
||||
### Ghi đè URL WASM
|
||||
|
||||
Bạn có thể ghi đè các giá trị mặc định lúc dựng cho các triển khai tùy chỉnh:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Để tắt hoàn toàn một mô-đun (yêu cầu người dùng tự cấu hình qua Cài đặt nâng cao), hãy đặt biến của nó thành chuỗi rỗng.
|
||||
|
||||
Với OCR, hoặc để trống tất cả biến `VITE_TESSERACT_*` và dùng các tài nguyên trực tuyến mặc định, hoặc đặt đồng thời các URL worker/core/lang cho OCR tự lưu trữ / ngoại tuyến. Nếu bạn chỉ đóng gói một số ngôn ngữ OCR, hãy đặt thêm `VITE_TESSERACT_AVAILABLE_LANGUAGES` với cùng các mã phân tách bằng dấu phẩy để giao diện chỉ đưa ra các ngôn ngữ đã cài và các lựa chọn không được hỗ trợ báo lỗi mô tả rõ ràng. Để xuất PDF có thể tìm kiếm hoàn toàn ngoại tuyến, hãy đặt thêm `VITE_OCR_FONT_BASE_URL` trỏ đến thư mục nội bộ phục vụ các phông chữ OCR đi kèm.
|
||||
|
||||
Người dùng cũng có thể ghi đè các giá trị mặc định này bất cứ lúc nào qua **Cài đặt nâng cao** trong giao diện: các ghi đè của người dùng lưu trong trình duyệt được ưu tiên hơn giá trị mặc định của môi trường.
|
||||
|
||||
### Triển khai cách ly mạng / ngoại tuyến
|
||||
|
||||
Dành cho các mạng không có internet (chính phủ, y tế, tài chính, v.v.). Các URL WASM được đưa thẳng vào JavaScript **lúc dựng**, còn các tệp WASM thực tế được **trình duyệt của người dùng** tải về lúc chạy. Vì vậy bạn cần chuẩn bị mọi thứ trên một máy có internet rồi chuyển vào mạng cách ly.
|
||||
|
||||
#### Script tự động (khuyến nghị)
|
||||
|
||||
Script `prepare-airgap.sh` đi kèm tự động hóa toàn bộ quy trình: tải các gói WASM, dựng image Docker và tạo một bundle độc lập cùng script thiết lập.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Lệnh này tạo ra một thư mục bundle:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
Chuyển bundle vào mạng cách ly qua USB, kho artifact nội bộ hoặc phương thức được phê duyệt. Sau đó chạy script thiết lập đi kèm:
|
||||
|
||||
```bash
|
||||
cd bentopdf-airgap-bundle
|
||||
bash setup.sh
|
||||
```
|
||||
|
||||
Script thiết lập sẽ nạp image Docker, giải nén các tệp WASM và tùy chọn khởi động container.
|
||||
|
||||
**Tùy chọn của script:**
|
||||
|
||||
| Flag | Mô tả | Mặc định |
|
||||
| ------------------------------ | ------------------------------------------------------ | ------------------------------ |
|
||||
| `--wasm-base-url <url>` | Nơi các WASM sẽ được lưu trữ nội bộ | _(bắt buộc, sẽ hỏi nếu thiếu)_ |
|
||||
| `--image-name <name>` | Tag image Docker | `bentopdf` |
|
||||
| `--output-dir <path>` | Thư mục bundle đầu ra | `./bentopdf-airgap-bundle` |
|
||||
| `--simple-mode` | Bật Simple Mode | tắt |
|
||||
| `--base-url <path>` | URL gốc của thư mục con (ví dụ `/pdf/`) | `/` |
|
||||
| `--language <code>` | Ngôn ngữ giao diện mặc định (ví dụ `fr`, `de`) | _(không có)_ |
|
||||
| `--brand-name <name>` | Tên thương hiệu tùy chỉnh | _(không có)_ |
|
||||
| `--brand-logo <path>` | Đường dẫn logo, tính từ `public/` | _(không có)_ |
|
||||
| `--footer-text <text>` | Văn bản chân trang tùy chỉnh | _(không có)_ |
|
||||
| `--ocr-languages <list>` | Các ngôn ngữ OCR cần đóng gói, phân tách bằng dấu phẩy | `eng` |
|
||||
| `--list-ocr-languages` | In các mã và tên OCR được hỗ trợ rồi thoát | tắt |
|
||||
| `--search-ocr-language <term>` | Tìm mã OCR theo tên hoặc chữ viết tắt | tắt |
|
||||
| `--dockerfile <path>` | Dockerfile cần dùng | `Dockerfile` |
|
||||
| `--skip-docker` | Bỏ qua bước dựng và xuất Docker | tắt |
|
||||
| `--skip-wasm` | Bỏ qua tải WASM (dùng lại các tệp `.tgz` có sẵn) | tắt |
|
||||
|
||||
Lời nhắc tương tác cũng chấp nhận `list` để in toàn bộ danh sách mã Tesseract được hỗ trợ và `search <term>` để tìm kết quả khớp như `search german` hoặc `search chi`.
|
||||
|
||||
::: warning Yêu cầu cùng origin
|
||||
Các tệp WASM phải được phục vụ từ **cùng origin** với ứng dụng BentoPDF. Web Worker dùng `importScripts()` và không thể tải script cross-origin. Ví dụ, nếu BentoPDF chạy tại `https://internal.example.com`, URL gốc của WASM cũng nên là `https://internal.example.com/wasm`.
|
||||
:::
|
||||
|
||||
#### Các bước thủ công
|
||||
|
||||
<details>
|
||||
<summary>Nếu bạn muốn làm thủ công mà không dùng script</summary>
|
||||
|
||||
**Bước 1: Tải các gói WASM và OCR** (trên máy có internet)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
**Bước 2: Dựng image Docker với các URL nội bộ**
|
||||
|
||||
```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 .
|
||||
```
|
||||
|
||||
**Bước 3: Xuất image Docker**
|
||||
|
||||
```bash
|
||||
docker save bentopdf -o bentopdf.tar
|
||||
```
|
||||
|
||||
**Bước 4: Chuyển vào mạng cách ly**
|
||||
|
||||
Sao chép qua USB, kho artifact nội bộ hoặc phương thức chuyển được phê duyệt:
|
||||
|
||||
- `bentopdf.tar`: image Docker
|
||||
- Năm gói WASM/OCR `.tgz` từ Bước 1
|
||||
- Thư mục `tesseract-langdata/` từ Bước 1
|
||||
- Thư mục `ocr-fonts/` từ Bước 1
|
||||
|
||||
**Bước 5: Thiết lập bên trong mạng cách ly**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Hãy đảm bảo các tệp truy cập được tại các URL bạn đã cấu hình ở Bước 2, gồm `.../ocr/worker.min.js`, `.../ocr/core`, `.../ocr/lang-data` và `.../ocr/fonts`.
|
||||
|
||||
</details>
|
||||
|
||||
::: info Dựng từ mã nguồn thay vì Docker?
|
||||
Đặt các biến trong `.env.production` trước khi chạy `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
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Tự lưu trữ WASM Proxy
|
||||
|
||||
Nếu bạn cần phục vụ các tệp WASM AGPL với header CORS phù hợp, bạn có thể triển khai một proxy đơn giản. Xem [hướng dẫn Cloudflare WASM Proxy](https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md) để có ví dụ triển khai.
|
||||
|
||||
::: tip Tại sao tách riêng?
|
||||
Việc tách riêng này đảm bảo:
|
||||
|
||||
- Tuân thủ pháp lý rõ ràng cho người dùng thương mại
|
||||
- Phần lõi của BentoPDF vẫn nằm dưới giấy phép kép (AGPL-3.0 / Commercial)
|
||||
- Các tệp WASM được tải lúc chạy, không đóng gói trong mã nguồn
|
||||
:::
|
||||
|
||||
## Yêu cầu hệ thống
|
||||
|
||||
| Yêu cầu | Tối thiểu |
|
||||
| ------- | ----------------------------------------- |
|
||||
| Lưu trữ | ~100 MB (phần lõi, không gồm mô-đun AGPL) |
|
||||
| RAM | 512 MB |
|
||||
| CPU | Bất kỳ bộ xử lý hiện đại nào |
|
||||
|
||||
::: tip
|
||||
BentoPDF là một trang web tĩnh, không cần cơ sở dữ liệu hay máy chủ backend!
|
||||
:::
|
||||
@@ -0,0 +1,211 @@
|
||||
# Triển khai với Kubernetes
|
||||
|
||||
Kubernetes có thể là quá mức cần thiết cho một trang web tĩnh, nhưng sẽ rất phù hợp nếu bạn đã chuẩn hóa theo Helm + GitOps.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Header bắt buộc để chuyển đổi tệp Office**
|
||||
>
|
||||
> Các công cụ dựa trên LibreOffice (chuyển đổi Word, Excel, PowerPoint) cần các header HTTP sau để hỗ trợ `SharedArrayBuffer`:
|
||||
>
|
||||
> - `Cross-Origin-Opener-Policy: same-origin`
|
||||
> - `Cross-Origin-Embedder-Policy: require-corp`
|
||||
>
|
||||
> Các image nginx chính thức của BentoPDF đã bao gồm các header này. Trong Kubernetes, **bộ điều khiển Ingress/Gateway cũng là reverse proxy**, vì vậy hãy đảm bảo các header này được giữ nguyên (hoặc thêm chúng ở lớp biên).
|
||||
|
||||
## Điều kiện tiên quyết
|
||||
|
||||
- Cụm Kubernetes
|
||||
- Helm v3
|
||||
- Một image nginx của BentoPDF (ví dụ `ghcr.io/alam00000/bentopdf:<tag>`) phục vụ trên **cổng 8080**
|
||||
|
||||
## Triển khai với Helm
|
||||
|
||||
### Cài đặt từ repo này (chart cục bộ)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Cài đặt từ GHCR (chart OCI)
|
||||
|
||||
Nếu chart được xuất bản lên GHCR dưới dạng artifact 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
|
||||
```
|
||||
|
||||
## Công khai dịch vụ
|
||||
|
||||
### Port-forward (thử nhanh)
|
||||
|
||||
```bash
|
||||
kubectl -n bentopdf port-forward deploy/bentopdf 8080:8080
|
||||
```
|
||||
|
||||
### Ingress (tùy chọn)
|
||||
|
||||
Bật Ingress (ví dụ cho nginx-ingress):
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: pdf.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
### Gateway API (tùy chọn)
|
||||
|
||||
Chart này hỗ trợ Gateway API `Gateway` + `HTTPRoute`.
|
||||
|
||||
Ví dụ (operator 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
|
||||
```
|
||||
|
||||
## Đảm bảo các header SharedArrayBuffer vẫn hoạt động (Ingress/Gateway)
|
||||
|
||||
### Điều "đáng lẽ" xảy ra
|
||||
|
||||
Cấu hình nginx của BentoPDF đặt các header phản hồi cần thiết. Hầu hết bộ điều khiển Ingress/Gateway **chuyển tiếp nguyên vẹn các header phản hồi từ upstream**.
|
||||
|
||||
### Điều có thể làm hỏng
|
||||
|
||||
- Một chính sách của bộ điều khiển/lớp biên **ghi đè** hoặc **loại bỏ** các header phản hồi
|
||||
- Một middleware "security headers" đặt giá trị COOP/COEP khác
|
||||
|
||||
### Cách kiểm tra
|
||||
|
||||
Chạy lệnh này với endpoint công khai của bạn:
|
||||
|
||||
```bash
|
||||
curl -I https://pdf.example.com/ | egrep -i 'cross-origin-opener-policy|cross-origin-embedder-policy'
|
||||
```
|
||||
|
||||
Bạn sẽ thấy:
|
||||
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
|
||||
### Nếu bộ điều khiển Ingress của bạn không giữ nguyên chúng
|
||||
|
||||
Hãy thêm header ở lớp biên (tùy theo bộ điều khiển). Ví dụ cho **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;
|
||||
```
|
||||
|
||||
## Lỗi kiểu MIME của `.mjs` (Ký PDF / iframe Form Filler bị trống)
|
||||
|
||||
Nếu bảng điều khiển trình duyệt hiển thị `Failed to load module script: ... non-JavaScript MIME type "application/octet-stream"` ở một yêu cầu `.mjs`, nghĩa là một bộ điều khiển Ingress/Gateway đặt phía trước nginx của BentoPDF đang thay header `Content-Type` từ upstream bằng `application/octet-stream`.
|
||||
|
||||
Nginx trong image BentoPDF vốn đã phục vụ `.mjs` với kiểu `application/javascript`. Cách khắc phục là ngăn bộ điều khiển loại bỏ/thay thế nó. Với nginx-ingress:
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
location ~* \.mjs$ {
|
||||
types {} default_type application/javascript;
|
||||
}
|
||||
```
|
||||
|
||||
Hoặc dùng bộ lọc `ResponseHeaderModifier` trong Gateway API để đặt `Content-Type: application/javascript` cho các đường dẫn `.mjs`. Kiểm tra bằng DevTools → tab Network: yêu cầu `.mjs` từng lỗi giờ sẽ hiển thị `content-type: application/javascript`.
|
||||
|
||||
### Nếu bạn dùng Gateway API và muốn buộc thêm header
|
||||
|
||||
Gateway API hỗ trợ bộ lọc `ResponseHeaderModifier`. Bạn có thể gắn nó trong `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
|
||||
```
|
||||
|
||||
Việc hỗ trợ từng bộ lọc cụ thể tùy thuộc vào bộ điều khiển Gateway của bạn; nếu một bộ lọc bị bỏ qua, hãy thêm header ở lớp biên/bộ điều khiển.
|
||||
|
||||
## Tắt công cụ cụ thể
|
||||
|
||||
Dùng ConfigMap để tắt công cụ lúc chạy mà không cần dựng lại image:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bentopdf-config
|
||||
namespace: bentopdf
|
||||
data:
|
||||
config.json: |
|
||||
{
|
||||
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
|
||||
}
|
||||
```
|
||||
|
||||
Gắn nó vào thư mục được phục vụ:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
ID công cụ là URL của trang bỏ đi `.html`. Hãy mở bất kỳ công cụ nào và xem URL (ví dụ: `edit-pdf`, `merge-pdf`, `compress-pdf`). Công cụ bị tắt sẽ bị ẩn khỏi trang chủ, tìm kiếm, phím tắt, trình tạo quy trình và truy cập trực tiếp bằng URL. Xem [hướng dẫn Docker](/vi/self-hosting/docker#tat-mot-so-cong-cu) để biết đầy đủ các tùy chọn.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Triển khai lên Netlify
|
||||
|
||||
[Netlify](https://netlify.com) cung cấp dịch vụ lưu trữ trang web tĩnh xuất sắc với gói miễn phí hào phóng.
|
||||
|
||||
## Triển khai một lần nhấp
|
||||
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/alam00000/bentopdf)
|
||||
|
||||
## Triển khai thủ công
|
||||
|
||||
### Bước 1: Kết nối repository
|
||||
|
||||
1. Đăng nhập vào [Netlify](https://app.netlify.com)
|
||||
2. Nhấp "Add new site" → "Import an existing project"
|
||||
3. Kết nối tài khoản GitHub của bạn
|
||||
4. Chọn bản fork BentoPDF của bạn
|
||||
|
||||
### Bước 2: Cấu hình thiết lập dựng
|
||||
|
||||
| Thiết lập | Giá trị |
|
||||
| ----------------- | --------------- |
|
||||
| Build command | `npm run build` |
|
||||
| Publish directory | `dist` |
|
||||
| Node version | 18+ |
|
||||
|
||||
### Bước 3: Triển khai
|
||||
|
||||
Nhấp "Deploy site" và đợi quá trình dựng hoàn tất.
|
||||
|
||||
## Tệp cấu hình
|
||||
|
||||
Tạo `netlify.toml` ở thư mục gốc của dự án:
|
||||
|
||||
```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 Quan trọng
|
||||
Các header `Cross-Origin-Embedder-Policy` và `Cross-Origin-Opener-Policy` là bắt buộc cho việc chuyển Word/ODT/Excel/PowerPoint sang PDF. Thiếu chúng, `SharedArrayBuffer` sẽ không khả dụng và engine LibreOffice WASM sẽ không khởi tạo được.
|
||||
|
||||
Các header `Content-Encoding: gzip` trên tệp `.wasm.gz` và `.data.gz` báo cho trình duyệt tự động giải nén chúng. Thiếu chúng, trình duyệt nhận dữ liệu gzip thô và quá trình biên dịch WASM sẽ thất bại.
|
||||
:::
|
||||
|
||||
## Biến môi trường
|
||||
|
||||
Đặt các biến này tại Site settings → Environment variables:
|
||||
|
||||
| Variable | Mô tả |
|
||||
| ----------------------- | ------------------------------------------------------------- |
|
||||
| `SIMPLE_MODE` | Đặt là `true` để dựng bản tối giản |
|
||||
| `VITE_BRAND_NAME` | Tên thương hiệu tùy chỉnh (thay cho "BentoPDF") |
|
||||
| `VITE_BRAND_LOGO` | Đường dẫn logo tính từ `public/` (ví dụ `images/my-logo.svg`) |
|
||||
| `VITE_FOOTER_TEXT` | Văn bản chân trang / bản quyền tùy chỉnh |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Ngôn ngữ giao diện mặc định (ví dụ `fr`, `de`, `es`) |
|
||||
|
||||
## Tên miền tùy chỉnh
|
||||
|
||||
1. Vào Site settings → Domain management
|
||||
2. Nhấp "Add custom domain"
|
||||
3. Làm theo hướng dẫn cấu hình DNS
|
||||
|
||||
## Phương tiện dung lượng lớn
|
||||
|
||||
Với các tệp WASM lớn, hãy cân nhắc bật [Netlify Large Media](https://docs.netlify.com/large-media/overview/):
|
||||
|
||||
```bash
|
||||
netlify lm:setup
|
||||
git lfs track "*.wasm"
|
||||
```
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### Chuyển Word/ODT/Excel sang PDF bị đứng ở 55%
|
||||
|
||||
Nếu quá trình chuyển đổi tài liệu bị treo ở 55%, hãy mở Console của DevTools và kiểm tra:
|
||||
|
||||
```js
|
||||
console.log(window.crossOriginIsolated); // should be true
|
||||
console.log(typeof SharedArrayBuffer); // should be "function"
|
||||
```
|
||||
|
||||
Nếu `crossOriginIsolated` là `false`, các header COEP/COOP từ `netlify.toml` của bạn chưa được áp dụng. Hãy đảm bảo tệp nằm ở thư mục gốc dự án rồi triển khai lại.
|
||||
|
||||
Nếu bạn thấy `expected magic word 00 61 73 6d, found 1f 8b 08 08` trong console, các tệp `.wasm.gz` đang thiếu header `Content-Encoding: gzip`. Hãy đảm bảo các khối `[[headers]]` cho `soffice.wasm.gz` và `soffice.data.gz` có trong `netlify.toml` của bạn.
|
||||
|
||||
### Dựng thất bại
|
||||
|
||||
Kiểm tra tính tương thích phiên bản Node:
|
||||
|
||||
```toml
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
```
|
||||
|
||||
### Tải lần đầu chậm
|
||||
|
||||
Bật tối ưu hóa tài nguyên tại Site settings → Build & deploy → Asset optimization.
|
||||
@@ -0,0 +1,232 @@
|
||||
# Triển khai với Nginx
|
||||
|
||||
Lưu trữ BentoPDF trên máy chủ của riêng bạn bằng Nginx.
|
||||
|
||||
## Điều kiện tiên quyết
|
||||
|
||||
- Máy chủ Ubuntu/Debian
|
||||
- Đã cài Nginx
|
||||
- Chứng chỉ SSL (khuyến nghị: Let's Encrypt)
|
||||
|
||||
## Bước 1: Dựng dự án
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Để tùy chỉnh thương hiệu, hãy đặt biến môi trường trước khi dựng:
|
||||
|
||||
```bash
|
||||
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
|
||||
```
|
||||
|
||||
## Bước 2: Sao chép tệp
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/bentopdf
|
||||
sudo cp -r dist/* /var/www/bentopdf/
|
||||
sudo chown -R www-data:www-data /var/www/bentopdf
|
||||
```
|
||||
|
||||
## Bước 3: Cấu hình Nginx
|
||||
|
||||
Tạo `/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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bước 4: Bật trang web
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Bước 5: SSL với Let's Encrypt
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
## Triển khai vào thư mục con
|
||||
|
||||
Để lưu trữ tại `/pdf/`:
|
||||
|
||||
```nginx
|
||||
location /pdf/ {
|
||||
alias /var/www/bentopdf/;
|
||||
try_files $uri $uri/ /pdf/index.html;
|
||||
}
|
||||
```
|
||||
|
||||
Dựng với:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf/ npm run build
|
||||
```
|
||||
|
||||
## Tinh chỉnh hiệu năng
|
||||
|
||||
Thêm vào `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;
|
||||
}
|
||||
```
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### WASM không tải được
|
||||
|
||||
Đảm bảo đã đặt kiểu MIME:
|
||||
|
||||
```nginx
|
||||
types {
|
||||
application/wasm wasm;
|
||||
}
|
||||
```
|
||||
|
||||
### Ký PDF hoặc Form Filler hiển thị trình xem trống (lỗi MIME `.mjs`)
|
||||
|
||||
Nếu bảng điều khiển trình duyệt hiển thị:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
...và yêu cầu bị lỗi có đuôi `.mjs`, nginx của bạn đang thiếu ánh xạ `.mjs → application/javascript`. Trình xem PDF đi kèm (được dùng bởi Ký PDF, Form Filler và một vài công cụ khác) có các tệp module ES với phần mở rộng `.mjs`, và `mime.types` mặc định của nginx không bao gồm chúng.
|
||||
|
||||
Hãy khắc phục bằng cách thêm `mjs` vào dòng `application/javascript` hiện có trong `/etc/nginx/mime.types`:
|
||||
|
||||
```nginx
|
||||
application/javascript js mjs;
|
||||
```
|
||||
|
||||
> KHÔNG thêm dòng `application/javascript` _thứ hai_: nginx phân tích `mime.types` theo từng dòng và chỉ một ánh xạ kiểu sang phần mở rộng có hiệu lực, nên dòng thứ hai bị bỏ qua âm thầm trên một số phiên bản. Hãy sửa dòng hiện có để thêm `mjs` cạnh `js`.
|
||||
|
||||
Hoặc ghi đè cục bộ trong khối server của bạn (không sửa tệp toàn cục):
|
||||
|
||||
```nginx
|
||||
types {
|
||||
application/javascript js mjs;
|
||||
application/wasm wasm;
|
||||
}
|
||||
```
|
||||
|
||||
Tải lại nginx sau khi thay đổi:
|
||||
|
||||
```bash
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Để xác nhận: trong DevTools của trình duyệt → tab Network, tìm yêu cầu `.mjs` bị lỗi và kiểm tra header phản hồi `Content-Type`. Giờ nó phải là `application/javascript`, không phải `application/octet-stream`.
|
||||
|
||||
### Chuyển Word/ODT/Excel sang PDF không hoạt động
|
||||
|
||||
LibreOffice WASM cần `SharedArrayBuffer`, thứ đòi hỏi các header `Cross-Origin-Embedder-Policy` và `Cross-Origin-Opener-Policy`. Nó cũng cần ngữ cảnh bảo mật, nên `http://localhost` dùng được để thử cục bộ nhưng `http://192.168.x.x` hay các IP LAN khác thường cần HTTPS. Lưu ý rằng các chỉ thị nginx `add_header` trong khối `location` **ghi đè** các chỉ thị `add_header` ở cấp server, chúng không được gộp lại. Mỗi khối `location` có `add_header` riêng phải bao gồm các header COEP/COOP.
|
||||
|
||||
Kiểm tra bằng:
|
||||
|
||||
```bash
|
||||
curl -I https://your-domain.com/ | grep -i cross-origin
|
||||
```
|
||||
|
||||
Nếu dùng reverse proxy đặt trước nginx, hãy đảm bảo nó giữ nguyên các header này.
|
||||
|
||||
### 502 Bad Gateway
|
||||
|
||||
Kiểm tra nhật ký lỗi của Nginx:
|
||||
|
||||
```bash
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
# Triển khai lên Vercel
|
||||
|
||||
[Vercel](https://vercel.com) mang lại trải nghiệm triển khai dễ nhất cho các trang web tĩnh.
|
||||
|
||||
## Triển khai một lần nhấp
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/alam00000/bentopdf)
|
||||
|
||||
## Triển khai thủ công
|
||||
|
||||
### Bước 1: Fork repository
|
||||
|
||||
Fork [bentopdf/bentopdf](https://github.com/alam00000/bentopdf) về tài khoản GitHub của bạn.
|
||||
|
||||
### Bước 2: Nhập vào Vercel
|
||||
|
||||
1. Truy cập [vercel.com/new](https://vercel.com/new)
|
||||
2. Chọn repository đã fork của bạn
|
||||
3. Cấu hình dự án:
|
||||
|
||||
| Thiết lập | Giá trị |
|
||||
| ---------------- | --------------- |
|
||||
| Framework Preset | Vite |
|
||||
| Build Command | `npm run build` |
|
||||
| Output Directory | `dist` |
|
||||
| Install Command | `npm install` |
|
||||
|
||||
### Bước 3: Biến môi trường (tùy chọn)
|
||||
|
||||
Thêm các biến này nếu cần:
|
||||
|
||||
| Variable | Mô tả |
|
||||
| ----------------------- | ------------------------------------------------------------- |
|
||||
| `SIMPLE_MODE` | Đặt là `true` để có giao diện tối giản |
|
||||
| `VITE_BRAND_NAME` | Tên thương hiệu tùy chỉnh (thay cho "BentoPDF") |
|
||||
| `VITE_BRAND_LOGO` | Đường dẫn logo tính từ `public/` (ví dụ `images/my-logo.svg`) |
|
||||
| `VITE_FOOTER_TEXT` | Văn bản chân trang / bản quyền tùy chỉnh |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Ngôn ngữ giao diện mặc định (ví dụ `fr`, `de`, `es`) |
|
||||
|
||||
### Bước 4: Triển khai
|
||||
|
||||
Nhấp "Deploy" và đợi quá trình dựng hoàn tất.
|
||||
|
||||
## Tên miền tùy chỉnh
|
||||
|
||||
1. Vào phần cài đặt dự án của bạn
|
||||
2. Chuyển đến "Domains"
|
||||
3. Thêm tên miền tùy chỉnh của bạn
|
||||
4. Cấu hình DNS theo hướng dẫn
|
||||
|
||||
## Hạn chế
|
||||
|
||||
::: warning Tệp lớn
|
||||
Các serverless function của Vercel có giới hạn 50MB. Vì BentoPDF là trang web tĩnh nên điều này không ảnh hưởng đến bạn, nhưng các mô-đun WASM khá lớn (tổng cộng ~100MB). Hãy đảm bảo chúng được phục vụ từ thư mục `/public`.
|
||||
:::
|
||||
|
||||
## Khắc phục sự cố
|
||||
|
||||
### Hết thời gian dựng
|
||||
|
||||
Nếu quá trình dựng bị hết thời gian, hãy thử:
|
||||
|
||||
```json
|
||||
// vercel.json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist"
|
||||
}
|
||||
```
|
||||
|
||||
### Lỗi 404 khi làm mới trang
|
||||
|
||||
Thêm `vercel.json` để định tuyến SPA:
|
||||
|
||||
```json
|
||||
{
|
||||
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
|
||||
}
|
||||
```
|
||||
|
||||
### Chuyển Word/ODT/Excel sang PDF không hoạt động
|
||||
|
||||
Việc chuyển đổi LibreOffice WASM cần `SharedArrayBuffer`, thứ đòi hỏi các header phản hồi sau trên mọi trang:
|
||||
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
|
||||
Các tệp `.wasm.gz` và `.data.gz` đã nén sẵn cũng cần `Content-Encoding: gzip` để trình duyệt giải nén chúng.
|
||||
|
||||
Thêm các mục này vào `vercel.json` của bạn:
|
||||
|
||||
```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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Để kiểm tra, mở Console của DevTools và chạy `console.log(window.crossOriginIsolated)`: kết quả phải là `true`.
|
||||
Reference in new issue
Block a user