mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-13 09: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 @@
|
||||
# Desplegar con Apache
|
||||
|
||||
Aloja BentoPDF con Apache HTTP Server.
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- Apache 2.4+
|
||||
- mod_rewrite habilitado
|
||||
- Certificado SSL (recomendado)
|
||||
|
||||
## Paso 1: Compilar el proyecto
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Para personalizar la marca, define variables de entorno antes de compilar:
|
||||
|
||||
```bash
|
||||
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
|
||||
```
|
||||
|
||||
## Paso 2: Copiar los archivos
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/bentopdf
|
||||
sudo cp -r dist/* /var/www/bentopdf/
|
||||
sudo chown -R www-data:www-data /var/www/bentopdf
|
||||
```
|
||||
|
||||
## Paso 3: Configuración de Apache
|
||||
|
||||
Crea `/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>
|
||||
```
|
||||
|
||||
## Paso 4: .htaccess para el enrutamiento
|
||||
|
||||
Crea `/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
|
||||
```
|
||||
|
||||
## Paso 5: Habilitar los módulos necesarios
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo a2enmod ssl
|
||||
sudo a2enmod headers
|
||||
sudo a2enmod expires
|
||||
sudo a2enmod deflate
|
||||
```
|
||||
|
||||
## Paso 6: Habilitar el sitio
|
||||
|
||||
```bash
|
||||
sudo a2ensite bentopdf.conf
|
||||
sudo apache2ctl configtest
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
## Despliegue en un subdirectorio
|
||||
|
||||
Para alojarlo en `/pdf/`:
|
||||
|
||||
1. Compila con la URL base:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf/ npm run build
|
||||
```
|
||||
|
||||
2. Actualiza `.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
|
||||
```
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### Errores 404 de WASM
|
||||
|
||||
Asegúrate de que el tipo MIME esté configurado:
|
||||
|
||||
```apache
|
||||
AddType application/wasm .wasm
|
||||
```
|
||||
|
||||
### Firmar PDF o el rellenador de formularios muestra un visor en blanco (error MIME de `.mjs`)
|
||||
|
||||
Si la consola del navegador muestra:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
...y la solicitud que falla termina en `.mjs`, a tu configuración de Apache le falta la asignación `.mjs → application/javascript`. El visor de PDF incluido usa módulos ES con extensión `.mjs`, y el `mime.types` de serie de Apache no los incluye por defecto.
|
||||
|
||||
Añade esto a tu VirtualHost o a `.htaccess`:
|
||||
|
||||
```apache
|
||||
AddType application/javascript .mjs
|
||||
```
|
||||
|
||||
Después recarga Apache:
|
||||
|
||||
```bash
|
||||
sudo apache2ctl configtest && sudo systemctl reload apache2
|
||||
```
|
||||
|
||||
Para confirmarlo: en las DevTools del navegador, pestaña Network, busca la solicitud `.mjs` que falla y revisa la cabecera de respuesta `Content-Type`. Ahora debería ser `application/javascript`, no `application/octet-stream`.
|
||||
|
||||
### La reescritura no funciona
|
||||
|
||||
Comprueba que mod_rewrite esté habilitado:
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
```
|
||||
|
||||
### Word/ODT/Excel a PDF no funciona
|
||||
|
||||
LibreOffice WASM requiere `SharedArrayBuffer`, que necesita estas cabeceras:
|
||||
|
||||
```apache
|
||||
Header always set Cross-Origin-Embedder-Policy "require-corp"
|
||||
Header always set Cross-Origin-Opener-Policy "same-origin"
|
||||
```
|
||||
|
||||
También necesita un contexto seguro. `http://localhost` funciona para pruebas locales, pero `http://192.168.x.x` u otras IP de red local normalmente requieren HTTPS. Si las cabeceras están presentes pero `window.crossOriginIsolated` sigue siendo `false`, comprueba si la página se abre por HTTP simple desde un origen que no es de loopback.
|
||||
|
||||
Los archivos precomprimidos `.wasm.gz` y `.data.gz` también necesitan el `Content-Encoding` correcto:
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
Asegúrate de que `mod_headers` esté habilitado: `sudo a2enmod headers`
|
||||
|
||||
### Permiso denegado
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/bentopdf
|
||||
sudo chmod -R 755 /var/www/bentopdf
|
||||
```
|
||||
@@ -0,0 +1,200 @@
|
||||
# Desplegar en AWS S3 + CloudFront
|
||||
|
||||
Aloja BentoPDF en AWS para obtener el máximo control y escalabilidad.
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
User → CloudFront (CDN) → S3 (Static Files)
|
||||
```
|
||||
|
||||
## Paso 1: Crear el bucket de S3
|
||||
|
||||
```bash
|
||||
# Create bucket
|
||||
aws s3 mb s3://your-bentopdf-bucket --region us-east-1
|
||||
|
||||
# Enable static website hosting
|
||||
aws s3 website s3://your-bentopdf-bucket \
|
||||
--index-document index.html \
|
||||
--error-document index.html
|
||||
```
|
||||
|
||||
## Paso 2: Compilar y subir
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Paso 3: Crear la distribución de CloudFront
|
||||
|
||||
```bash
|
||||
aws cloudfront create-distribution \
|
||||
--origin-domain-name your-bentopdf-bucket.s3.amazonaws.com \
|
||||
--default-root-object index.html
|
||||
```
|
||||
|
||||
O usa la consola de AWS:
|
||||
|
||||
1. Ve a CloudFront → Create distribution
|
||||
2. Origin domain: selecciona tu bucket de S3
|
||||
3. Activa "Origin Access Control"
|
||||
4. Default root object: `index.html`
|
||||
5. Crea la distribución
|
||||
|
||||
## Paso 3b: Política de cabeceras de respuesta (necesaria para LibreOffice WASM)
|
||||
|
||||
Las conversiones basadas en LibreOffice (Word, Excel, PowerPoint a PDF) requieren `SharedArrayBuffer`, que necesita cabeceras de respuesta específicas. Crea una Response Headers Policy de CloudFront:
|
||||
|
||||
1. Ve a CloudFront → Policies → Response headers
|
||||
2. Crea una política personalizada con estas cabeceras:
|
||||
|
||||
| Cabecera | Valor |
|
||||
| ------------------------------ | -------------- |
|
||||
| `Cross-Origin-Embedder-Policy` | `require-corp` |
|
||||
| `Cross-Origin-Opener-Policy` | `same-origin` |
|
||||
| `Cross-Origin-Resource-Policy` | `cross-origin` |
|
||||
|
||||
3. Asocia la política al comportamiento de caché predeterminado de tu distribución
|
||||
|
||||
O mediante la 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}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Paso 3c: Metadatos de S3 para los archivos WASM precomprimidos
|
||||
|
||||
Los archivos WASM de LibreOffice están precomprimidos (`.wasm.gz`, `.data.gz`). Define el Content-Type y el Content-Encoding correctos para que los navegadores los descompriman:
|
||||
|
||||
```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 Importante
|
||||
Sin la política de cabeceras de respuesta, `SharedArrayBuffer` no está disponible y las conversiones de LibreOffice WASM se quedarán colgadas en torno al 55 %. Sin el Content-Encoding correcto en los archivos `.gz`, el navegador recibe bytes gzip sin procesar y la compilación de WASM falla.
|
||||
:::
|
||||
|
||||
## Paso 4: Política del bucket de S3
|
||||
|
||||
Permite que CloudFront acceda al 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Paso 5: Páginas de error personalizadas
|
||||
|
||||
Configura el 404 para que devuelva `index.html` y funcione el enrutamiento SPA:
|
||||
|
||||
1. CloudFront → Error pages
|
||||
2. Crea una respuesta de error personalizada:
|
||||
- HTTP error code: 404
|
||||
- Response page path: `/index.html`
|
||||
- HTTP response code: 200
|
||||
|
||||
## Estimación de costes
|
||||
|
||||
| Recurso | Coste estimado |
|
||||
| ---------------------------------- | -------------- |
|
||||
| Almacenamiento S3 (~500MB) | ~$0.01/mes |
|
||||
| CloudFront (1TB de transferencia) | ~$85/mes |
|
||||
| CloudFront (10GB de transferencia) | ~$0.85/mes |
|
||||
|
||||
::: tip
|
||||
Usa S3 Intelligent Tiering para optimizar costes en archivos a los que se accede con poca frecuencia.
|
||||
:::
|
||||
|
||||
## Automatización con 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 @@
|
||||
# Desplegar en Cloudflare Pages
|
||||
|
||||
[Cloudflare Pages](https://pages.cloudflare.com) ofrece alojamiento estático global y rápido con ancho de banda ilimitado.
|
||||
|
||||
## Despliegue rápido
|
||||
|
||||
1. Ve a [Cloudflare Pages](https://dash.cloudflare.com/?to=/:account/pages)
|
||||
2. Haz clic en "Create a project"
|
||||
3. Conecta tu repositorio de GitHub
|
||||
|
||||
## Configuración de compilación
|
||||
|
||||
| Ajuste | Valor |
|
||||
| ---------------------- | --------------- |
|
||||
| Framework preset | None |
|
||||
| Build command | `npm run build` |
|
||||
| Build output directory | `dist` |
|
||||
| Root directory | `/` |
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
Añádelas en Settings → Environment variables:
|
||||
|
||||
| Variable | Value |
|
||||
| ----------------------- | ------------------------------------------------------------ |
|
||||
| `NODE_VERSION` | `18` |
|
||||
| `SIMPLE_MODE` | `false` (opcional) |
|
||||
| `VITE_BRAND_NAME` | Nombre de marca personalizado (opcional) |
|
||||
| `VITE_BRAND_LOGO` | Ruta del logo relativa a `public/` (opcional) |
|
||||
| `VITE_FOOTER_TEXT` | Texto personalizado de pie/copyright (opcional) |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Idioma predeterminado de la interfaz, p. ej. `fr` (opcional) |
|
||||
|
||||
## Archivo de configuración
|
||||
|
||||
Crea `_headers` en tu carpeta `public`:
|
||||
|
||||
```
|
||||
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
|
||||
/*
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Resource-Policy: cross-origin
|
||||
|
||||
# Pre-compressed LibreOffice WASM binary
|
||||
/libreoffice-wasm/soffice.wasm.gz
|
||||
Content-Type: application/wasm
|
||||
Content-Encoding: gzip
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# Pre-compressed LibreOffice WASM data
|
||||
/libreoffice-wasm/soffice.data.gz
|
||||
Content-Type: application/octet-stream
|
||||
Content-Encoding: gzip
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
# Cache WASM files aggressively
|
||||
/*.wasm
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
Content-Type: application/wasm
|
||||
|
||||
# Service worker
|
||||
/sw.js
|
||||
Cache-Control: no-cache
|
||||
```
|
||||
|
||||
::: warning Importante
|
||||
Las cabeceras `Cross-Origin-Embedder-Policy` y `Cross-Origin-Opener-Policy` son necesarias para las conversiones de Word/ODT/Excel/PowerPoint a PDF. Sin ellas, `SharedArrayBuffer` no está disponible y el motor LibreOffice WASM no se podrá inicializar.
|
||||
:::
|
||||
|
||||
Crea `_redirects` para el enrutamiento SPA:
|
||||
|
||||
```
|
||||
/* /index.html 200
|
||||
```
|
||||
|
||||
## Dominio personalizado
|
||||
|
||||
1. Ve a tu proyecto de Pages
|
||||
2. Haz clic en "Custom domains"
|
||||
3. Añade tu dominio
|
||||
4. Cloudflare configurará el DNS automáticamente si el dominio está en Cloudflare
|
||||
|
||||
## Ventajas
|
||||
|
||||
- **Ancho de banda ilimitado y gratuito**
|
||||
- **CDN global** con más de 300 ubicaciones edge
|
||||
- **HTTPS automático**
|
||||
- **Despliegues de vista previa** para pull requests
|
||||
- **Compilaciones rápidas**
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### Subida de archivos grandes
|
||||
|
||||
Cloudflare Pages admite archivos de hasta 25 MB. Los módulos WASM deberían caber, pero si llegas a los límites, considera:
|
||||
|
||||
```bash
|
||||
# Split large files during build
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Límites de tamaño del Worker
|
||||
|
||||
Si usas Cloudflare Workers para un enrutamiento avanzado, ten en cuenta el límite de 1 MB de los planes gratuitos.
|
||||
|
||||
## Worker de proxy CORS (para firmas digitales)
|
||||
|
||||
La herramienta de firma digital necesita un proxy CORS para obtener las cadenas de certificados. Despliega el worker incluido:
|
||||
|
||||
```bash
|
||||
cd cloudflare
|
||||
npx wrangler login
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
### Funciones de seguridad
|
||||
|
||||
| Función | Descripción |
|
||||
| --------------------------- | ------------------------------------ |
|
||||
| **Restricciones de URL** | Solo se permiten URL de certificados |
|
||||
| **Límite de tamaño** | Máx. 10MB por solicitud |
|
||||
| **Limitación de velocidad** | 60 solicitudes/IP/min (requiere KV) |
|
||||
| **Bloqueo de IP privadas** | Bloquea localhost e IP internas |
|
||||
|
||||
### Activar la limitación de velocidad
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Compilar con la URL del proxy
|
||||
|
||||
```bash
|
||||
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
|
||||
```
|
||||
|
||||
O con 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 .
|
||||
```
|
||||
|
||||
> **Nota:** Consulta el [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required) para configurar la firma HMAC.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Proxy CORS para obtener certificados
|
||||
|
||||
La herramienta de firma digital usa un proxy CORS para obtener los certificados de los emisores desde Autoridades de Certificación (CA) externas. Es necesario porque muchos servidores de CA no incluyen cabeceras CORS en sus respuestas, lo que impide la descarga directa desde el navegador.
|
||||
|
||||
Además, muchos servidores de CA sirven los certificados por HTTP simple. Cuando tu instancia de BentoPDF se aloja por HTTPS, los navegadores bloquean estas solicitudes HTTP (política de contenido mixto). El proxy CORS resuelve ambos problemas al enrutar las solicitudes a través de un endpoint HTTPS con las cabeceras adecuadas.
|
||||
|
||||
## Cómo funciona
|
||||
|
||||
Al firmar un PDF con un certificado:
|
||||
|
||||
1. La biblioteca `zgapdfsigner` intenta construir una cadena de certificados completa
|
||||
2. Obtiene los certificados de los emisores desde las URL incluidas en la extensión AIA (Authority Information Access) de tu certificado
|
||||
3. Estas solicitudes pasan por un proxy CORS que añade las cabeceras `Access-Control-Allow-Origin` necesarias
|
||||
4. El proxy devuelve los datos del certificado al navegador
|
||||
|
||||
## Autoalojar el proxy CORS
|
||||
|
||||
Si autoalojas BentoPDF, tendrás que desplegar tu propio proxy CORS para que las firmas digitales funcionen con certificados que requieren obtener la cadena.
|
||||
|
||||
### Opción 1: Cloudflare Workers (recomendada)
|
||||
|
||||
1. **Instala la CLI de Wrangler**:
|
||||
|
||||
```bash
|
||||
npm install -g wrangler
|
||||
```
|
||||
|
||||
2. **Inicia sesión en Cloudflare**:
|
||||
|
||||
```bash
|
||||
wrangler login
|
||||
```
|
||||
|
||||
3. **Clona BentoPDF y actualiza los orígenes permitidos**:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf/cloudflare
|
||||
```
|
||||
|
||||
Abre `cors-proxy-worker.js` y cambia el array `ALLOWED_ORIGINS` por tu dominio:
|
||||
|
||||
```js
|
||||
const ALLOWED_ORIGINS = [
|
||||
'https://your-domain.com',
|
||||
'https://www.your-domain.com',
|
||||
];
|
||||
```
|
||||
|
||||
::: warning Importante
|
||||
Sin este cambio, el proxy rechazará todas las solicitudes de tu sitio con un error **403 Forbidden**. Por defecto solo permite solicitudes de `bentopdf.com`.
|
||||
:::
|
||||
|
||||
4. **Despliega el proxy**:
|
||||
|
||||
```bash
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
Anota la URL de tu worker (p. ej., `https://bentopdf-cors-proxy.your-subdomain.workers.dev`).
|
||||
|
||||
5. **Vuelve a compilar BentoPDF con la URL del proxy**:
|
||||
|
||||
Si usas 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 .
|
||||
```
|
||||
|
||||
Si compilas desde el código fuente:
|
||||
|
||||
```bash
|
||||
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
|
||||
```
|
||||
|
||||
### Opción 2: Proxy de backend personalizado
|
||||
|
||||
También puedes crear tu propio endpoint de proxy. Los requisitos son:
|
||||
|
||||
1. Aceptar solicitudes GET con un parámetro de consulta `url`
|
||||
2. Obtener la URL desde tu servidor (sin restricciones CORS en el lado del servidor)
|
||||
3. Devolver la respuesta con estas cabeceras:
|
||||
- `Access-Control-Allow-Origin: https://your-domain.com`
|
||||
- `Access-Control-Allow-Methods: GET, OPTIONS`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
|
||||
Ejemplo de implementación con 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' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Consideraciones de seguridad
|
||||
|
||||
El Cloudflare Worker incluido tiene varias medidas de seguridad:
|
||||
|
||||
| Función | Descripción |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| **Validación de origen** | Solo permite solicitudes de los dominios incluidos en `ALLOWED_ORIGINS` |
|
||||
| **Restricciones de URL** | Solo permite URL de certificados (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`, `/crl`) |
|
||||
| **Bloqueo de IP privadas** | Bloquea rangos privados IPv4/IPv6, link-local, loopback, IP decimales y metadatos de la nube |
|
||||
| **Seguridad del Content-Type** | Solo devuelve tipos MIME de certificado seguros y bloquea la inyección de content-type desde el origen |
|
||||
| **Límite de tamaño** | Transmite la respuesta con un límite de 10MB y aborta la descarga si se supera |
|
||||
| **Limitación de velocidad** | 60 solicitudes por minuto por IP o IPv6 /64 (requiere KV, permite el paso si KV falla) |
|
||||
| **Firmas HMAC** | Firma opcional en el cliente (disuade el abuso casual) |
|
||||
|
||||
### Reenlace de DNS (DNS rebinding) al autoalojar fuera de Cloudflare
|
||||
|
||||
El worker rechaza cualquier URL que se resuelva a una IP privada o interna. No puede usar una lista fija de hosts permitidos, ya que las URL de la cadena de certificados apuntan a la CA que emitió el certificado (FNMT, CA corporativas, etc.), por lo que los destinos no se conocen de antemano. Eso deja una pequeña ventana de rebinding: quien controle el DNS de su propio dominio podría devolver una IP pública a la comprobación de seguridad y una privada a la solicitud real un instante después.
|
||||
|
||||
En Cloudflare esto no es explotable: los Workers no pueden alcanzar IP privadas ni el endpoint de metadatos `169.254.169.254`, así que el rebinding no llega a nada. Simplemente no le des al worker una vía de entrada: no le asocies un Tunnel, un Service ni un binding de red privada (solo necesita `RATE_LIMIT_KV`).
|
||||
|
||||
Si lo ejecutas fuera de Cloudflare (workerd, Node y similares) dentro de una VPC, el runtime _sí_ puede alcanzar IP internas. Coloca un firewall de salida delante y bloquea los rangos privados: `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` y sus equivalentes IPv6. El código de la aplicación no puede imponerlo una vez que estás fuera de Cloudflare.
|
||||
|
||||
## Desactivar el proxy
|
||||
|
||||
Si no quieres usar un proxy CORS, define la variable de entorno como una cadena vacía:
|
||||
|
||||
```
|
||||
VITE_CORS_PROXY_URL=
|
||||
```
|
||||
|
||||
**Nota**: Sin el proxy, firmar con certificados que requieren obtener la cadena externamente (como FNMT o algunas CA corporativas) fallará con el error "Failed to fetch".
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### "Signing error: TypeError: Failed to fetch"
|
||||
|
||||
Normalmente significa una de estas cosas:
|
||||
|
||||
1. **No hay proxy CORS configurado**: define `VITE_CORS_PROXY_URL` y vuelve a compilar
|
||||
2. **Contenido mixto bloqueado**: tu sitio usa HTTPS pero la URL del emisor del certificado es HTTP. El proxy CORS lo resuelve.
|
||||
3. **El proxy CORS rechaza tu origen**: comprueba que tu dominio esté en el array `ALLOWED_ORIGINS` de `cors-proxy-worker.js`
|
||||
|
||||
### "403 Forbidden" del proxy
|
||||
|
||||
Tu dominio no está en la lista `ALLOWED_ORIGINS`. Edita `cors-proxy-worker.js`:
|
||||
|
||||
```js
|
||||
const ALLOWED_ORIGINS = ['https://your-domain.com'];
|
||||
```
|
||||
|
||||
Después vuelve a desplegar: `npx wrangler deploy`
|
||||
|
||||
### Probar el proxy
|
||||
|
||||
```bash
|
||||
curl -H "Origin: https://your-domain.com" \
|
||||
"https://your-proxy.workers.dev?url=http://www.cert.fnmt.es/certs/ACUSU.crt"
|
||||
```
|
||||
|
||||
### Certificados que funcionan sin proxy
|
||||
|
||||
Algunos certificados incluyen la cadena completa en el archivo P12/PFX y no requieren obtención externa:
|
||||
|
||||
- Certificados autofirmados
|
||||
- Algunas CA comerciales que incluyen los certificados intermedios
|
||||
- Certificados que has ensamblado manualmente con la cadena completa
|
||||
@@ -0,0 +1,622 @@
|
||||
# Desplegar con Docker / Podman
|
||||
|
||||
La forma más sencilla de autoalojar BentoPDF en un entorno de producción.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Cabeceras necesarias para la conversión de archivos de Office**
|
||||
>
|
||||
> Las herramientas basadas en LibreOffice (conversión de Word, Excel y PowerPoint) requieren estas cabeceras HTTP para admitir `SharedArrayBuffer`:
|
||||
>
|
||||
> - `Cross-Origin-Opener-Policy: same-origin`
|
||||
> - `Cross-Origin-Embedder-Policy: require-corp`
|
||||
>
|
||||
> La página también debe servirse desde un contexto seguro. `http://localhost` funciona para pruebas locales, pero `http://192.168.x.x` u otras IP de red local normalmente no cumplen el requisito, así que la conversión de Office por HTTP simple fallará aunque las cabeceras estén presentes.
|
||||
>
|
||||
> Las imágenes de contenedor oficiales incluyen estas cabeceras. Si usas un proxy inverso (Traefik, Caddy, etc.), asegúrate de que estas cabeceras se conserven o se añadan, y usa HTTPS para el acceso que no sea loopback.
|
||||
|
||||
> [!TIP]
|
||||
> **Usuarios de Podman:** todos los comandos `docker` funcionan con Podman sustituyendo `docker` por `podman` y `docker-compose` por `podman-compose`.
|
||||
|
||||
## Inicio rápido
|
||||
|
||||
> [!TIP]
|
||||
> BentoPDF se distribuye en dos versiones:
|
||||
>
|
||||
> - **Versión autoalojada**: `ghcr.io/alam00000/bentopdf-simple:latest`. Todas las herramientas PDF, **sin** el aparato de marketing (sin hero, preguntas frecuentes, testimonios ni pie de página). Úsala para despliegues internos, de equipo u organización. **No** es una versión "lite" con menos funciones.
|
||||
> - **Versión comercial**: `ghcr.io/alam00000/bentopdf:latest`. El sitio de marketing completo, usado por el propio bentopdf.com y por los titulares de licencia comercial con despliegues públicos.
|
||||
>
|
||||
> Si tienes dudas: descarga la versión autoalojada.
|
||||
|
||||
```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
|
||||
|
||||
Crea `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
|
||||
```
|
||||
|
||||
Ejecuta:
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker compose up -d
|
||||
|
||||
# Podman Compose
|
||||
podman-compose up -d
|
||||
```
|
||||
|
||||
## Crear tu propia imagen
|
||||
|
||||
```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;"]
|
||||
```
|
||||
|
||||
Compila y ejecuta:
|
||||
|
||||
```bash
|
||||
docker build -t bentopdf:custom .
|
||||
docker run -d -p 3000:8080 bentopdf:custom
|
||||
```
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
| Variable | Descripción | Predeterminado |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
|
||||
| `SIMPLE_MODE` | Compila la variante autoalojada (oculta el aparato de marketing de bentopdf.com: hero, preguntas frecuentes, testimonios y pie de página). Se mantienen todas las herramientas PDF. | `false` |
|
||||
| `BASE_URL` | Desplegar en un subdirectorio | `/` |
|
||||
| `VITE_WASM_PYMUPDF_URL` | URL del módulo WASM de PyMuPDF | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
|
||||
| `VITE_WASM_GS_URL` | URL del módulo WASM de Ghostscript | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` |
|
||||
| `VITE_WASM_CPDF_URL` | URL del módulo WASM de CoherentPDF | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` |
|
||||
| `VITE_TESSERACT_WORKER_URL` | URL del script del worker de OCR | _(vacío; usa la CDN predeterminada de Tesseract.js)_ |
|
||||
| `VITE_TESSERACT_CORE_URL` | Directorio del runtime principal de OCR | _(empty; use Tesseract.js default CDN)_ |
|
||||
| `VITE_TESSERACT_LANG_URL` | Directorio de traineddata de OCR | _(empty; use Tesseract.js default CDN)_ |
|
||||
| `VITE_TESSERACT_AVAILABLE_LANGUAGES` | Idiomas de OCR separados por comas que se muestran en la interfaz | _(vacío; muestra el catálogo completo)_ |
|
||||
| `VITE_OCR_FONT_BASE_URL` | Directorio de fuentes de la capa de texto del OCR | _(vacío; usa las URL remotas de las fuentes Noto)_ |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Idioma predeterminado de la interfaz | `en` |
|
||||
| `VITE_BRAND_NAME` | Nombre de marca personalizado | `BentoPDF` |
|
||||
| `VITE_BRAND_LOGO` | Ruta del logo relativa a `public/` | `images/favicon-no-bg.svg` |
|
||||
| `VITE_FOOTER_TEXT` | Texto personalizado de pie/copyright | `© 2026 BentoPDF. All rights reserved.` |
|
||||
| `DISABLE_TOOLS` | IDs de herramientas separados por comas para ocultar | _(vacío; todas las herramientas activadas)_ |
|
||||
| `DISABLE_GITHUB_STARS` | Omite la obtención del contador de estrellas de GitHub al cargar la página y elimina `api.github.com` de la CSP. Las compilaciones en Simple Mode siempre la omiten. | `false` |
|
||||
| `SITE_URL` | URL pública escrita en los enlaces canonical, hreflang y de sitemap. Déjala por defecto salvo que quieras que los buscadores traten tu despliegue como un sitio propio. | `https://www.bentopdf.com` |
|
||||
| `ALLOW_INDEXING` | Con un `SITE_URL` personalizado, todas las páginas se marcan como `noindex` para que tu copia no compita con bentopdf.com en los resultados de búsqueda. Ponlo en `true` para que los buscadores la indexen. | `false` |
|
||||
|
||||
Las URL de los módulos WASM vienen preconfiguradas con valores predeterminados de CDN, así que todas las funciones avanzadas funcionan desde el primer momento. Sobrescríbelas para despliegues aislados de la red o autoalojados.
|
||||
|
||||
### Buscadores
|
||||
|
||||
Por defecto, todas las páginas indican a los buscadores que el original es bentopdf.com, de modo que tu despliegue no aparece como duplicado. No necesitas cambiar nada para una instancia privada o interna.
|
||||
|
||||
- **Mantener tu instancia fuera de los resultados de búsqueda por completo:** ejecuta el contenedor con `-e ROBOTS_NOINDEX=true`. Solo requiere un reinicio, no una nueva compilación.
|
||||
- **Hacer que tu instancia sea un sitio propio en los resultados de búsqueda:** compila con `--build-arg SITE_URL=https://your-domain.example --build-arg ALLOW_INDEXING=true`. Sin `ALLOW_INDEXING=true`, un `SITE_URL` personalizado marca todas las páginas como `noindex`.
|
||||
|
||||
### Content-Security-Policy
|
||||
|
||||
La imagen de nginx incluye una cabecera `Content-Security-Policy` en modo de aplicación. Las directivas `script-src`, `connect-src` y `font-src` de la CSP se **generan en tiempo de compilación** a partir de las variables de URL `VITE_*` anteriores: los hosts que pases mediante `--build-arg` se añaden automáticamente a la política.
|
||||
|
||||
Como resultado:
|
||||
|
||||
- Si sobrescribes `VITE_CORS_PROXY_URL` o `VITE_WASM_*_URL` al compilar, la CSP permite esos orígenes automáticamente, sin configuración adicional.
|
||||
- Para las URL en `cdn.jsdelivr.net`, `script-src` solo permite la carpeta exacta del paquete con su versión, por ejemplo `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/`, no toda la CDN. jsDelivr sirve cualquier paquete público de npm o repositorio de GitHub, así que permitir todo el host dejaría que cualquiera de ellos se ejecutara en tu instancia. Las URL de cualquier otro host se permiten por origen.
|
||||
- Si configuras URL de WASM personalizadas en _tiempo de ejecución_ desde la página de Ajustes avanzados de la aplicación, esos orígenes **no** están en la CSP y el navegador bloqueará las solicitudes a ellos. Esto también se aplica a una versión distinta del mismo paquete en `cdn.jsdelivr.net`, porque solo se permite la versión definida al compilar. La configuración en tiempo de ejecución está pensada para experimentar; para URL personalizadas permanentes, define el build arg `VITE_*` correspondiente.
|
||||
- Los despliegues aislados de la red que sobrescriben los tres valores `VITE_WASM_*_URL` también eliminan de la CSP esas carpetas de paquete de `cdn.jsdelivr.net` (cada valor predeterminado se sustituye, no se añade). Los valores predeterminados de Tesseract OCR en `cdn.jsdelivr.net` permanecen en la política hasta que también definas los tres valores `VITE_TESSERACT_*`. Del mismo modo, definir `VITE_CORS_PROXY_URL` sustituye el valor público predeterminado `bentopdf-cors-proxy.bentopdf.workers.dev`.
|
||||
|
||||
La CSP incluye `'unsafe-eval'` en `script-src` porque el runtime WASM de LibreOffice (usado por las herramientas de conversión de Word/Excel/PowerPoint) y la biblioteca de imágenes wasm-vips (usada por PDF a TIFF) compilan código de despacho interno mediante `new Function()`. Quitarlo rompería esas herramientas. Si compilas en `SIMPLE_MODE` (sin LibreOffice) y no necesitas PDF a TIFF, puedes editar manualmente el `security-headers.conf` generado para eliminar `'unsafe-eval'` y obtener una política más estricta.
|
||||
|
||||
Para el OCR, deja vacías las variables `VITE_TESSERACT_*` para usar los recursos en línea predeterminados, o define las tres juntas para un OCR autoalojado o sin conexión. Las sobrescrituras parciales de OCR se rechazan porque el worker, el runtime principal y el directorio de traineddata deben coincidir. Para una salida de PDF con texto buscable totalmente sin conexión, define también `VITE_OCR_FONT_BASE_URL` para que las fuentes de la capa de texto del OCR se carguen desde tu servidor interno en lugar de las URL públicas de las fuentes Noto.
|
||||
|
||||
`VITE_DEFAULT_LANGUAGE` define el idioma de la interfaz para quienes visitan por primera vez. Valores admitidos: `en`, `ar`, `be`, `fr`, `de`, `es`, `zh`, `zh-TW`, `vi`, `tr`, `id`, `it`, `pt`, `nl`, `da`. Los usuarios pueden seguir cambiando de idioma: esto solo cambia el valor predeterminado.
|
||||
|
||||
Ejemplo:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Marca personalizada
|
||||
|
||||
Sustituye el logo, el nombre y el texto de pie de BentoPDF por los tuyos. Coloca tu archivo de logo en la carpeta `public/` (o usa una imagen existente) y pasa las variables de marca al compilar:
|
||||
|
||||
```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 .
|
||||
```
|
||||
|
||||
La marca personalizada funciona tanto en el modo completo como en Simple Mode, y se puede combinar con todas las demás opciones de tiempo de compilación.
|
||||
|
||||
### Desactivar herramientas concretas
|
||||
|
||||
Oculta herramientas de la interfaz por requisitos de cumplimiento o seguridad. Las herramientas desactivadas desaparecen de la página de inicio, los resultados de búsqueda, los atajos de teclado y el constructor de flujos de trabajo. Si se accede directamente por URL, se muestra una página de "herramienta no disponible".
|
||||
|
||||
Los ID de herramienta son la URL de la página sin `.html`. Por ejemplo, si la herramienta está en `bentopdf.com/edit-pdf.html`, el ID es `edit-pdf`.
|
||||
|
||||
#### Encontrar los ID de herramienta
|
||||
|
||||
La forma más fácil: abre cualquier herramienta en BentoPDF y mira la URL. La última parte de la ruta (sin `.html`) es el ID de la herramienta.
|
||||
|
||||
<details>
|
||||
<summary>Lista completa de ID de herramientas</summary>
|
||||
|
||||
**Editar y anotar:** `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`
|
||||
|
||||
**Convertir a 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`
|
||||
|
||||
**Convertir desde 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`
|
||||
|
||||
**Organizar y gestionar:** `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`
|
||||
|
||||
**Optimizar y reparar:** `compress-pdf`, `pdf-to-pdfa`, `fix-page-size`, `linearize-pdf`, `remove-restrictions`, `repair-pdf`, `rasterize-pdf`, `deskew-pdf`, `font-to-outline`
|
||||
|
||||
**Seguridad:** `encrypt-pdf`, `sanitize-pdf`, `decrypt-pdf`, `flatten-pdf`, `remove-metadata`, `change-permissions`, `digital-sign-pdf`, `validate-signature-pdf`, `timestamp-pdf`
|
||||
|
||||
</details>
|
||||
|
||||
#### Opción 1: En tiempo de compilación (build arg de Docker)
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--build-arg DISABLE_TOOLS="edit-pdf,sign-pdf,encrypt-pdf" \
|
||||
-t bentopdf .
|
||||
```
|
||||
|
||||
Esto incorpora la lista de herramientas desactivadas en el bundle de JavaScript. Para cambiarla hay que volver a compilar.
|
||||
|
||||
#### Opción 2: En tiempo de ejecución (config.json)
|
||||
|
||||
Monta un archivo `config.json` en el directorio servido, sin necesidad de recompilar:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
O con 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
|
||||
```
|
||||
|
||||
Ambos métodos se pueden combinar: las listas se fusionan. Si una herramienta aparece en cualquiera de ellas, se desactiva.
|
||||
|
||||
#### Desactivar funciones del editor
|
||||
|
||||
También puedes desactivar funciones concretas dentro del editor de PDF (p. ej., censura, anotaciones, formularios) sin desactivar toda la herramienta del editor. Añade `editorDisabledCategories` a tu `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"editorDisabledCategories": ["redaction", "annotation-stamp"]
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Lista completa de categorías del editor</summary>
|
||||
|
||||
**Zoom:** `zoom`, `zoom-in`, `zoom-out`, `zoom-fit-page`, `zoom-fit-width`, `zoom-marquee`, `zoom-level`
|
||||
|
||||
**Anotación:** `annotation`, `annotation-markup`, `annotation-highlight`, `annotation-underline`, `annotation-strikeout`, `annotation-squiggly`, `annotation-ink`, `annotation-text`, `annotation-stamp`
|
||||
|
||||
**Formas:** `annotation-shape`, `annotation-rectangle`, `annotation-circle`, `annotation-line`, `annotation-arrow`, `annotation-polygon`, `annotation-polyline`
|
||||
|
||||
**Formulario:** `form`, `form-textfield`, `form-checkbox`, `form-radio`, `form-select`, `form-listbox`, `form-fill-mode`
|
||||
|
||||
**Censura:** `redaction`, `redaction-area`, `redaction-text`, `redaction-apply`, `redaction-clear`
|
||||
|
||||
**Documento:** `document`, `document-open`, `document-close`, `document-print`, `document-capture`, `document-export`, `document-fullscreen`, `document-protect`
|
||||
|
||||
**Página:** `page`, `spread`, `rotate`, `scroll`, `navigation`
|
||||
|
||||
**Panel:** `panel`, `panel-sidebar`, `panel-search`, `panel-comment`
|
||||
|
||||
**Herramientas:** `tools`, `pan`, `pointer`, `capture`
|
||||
|
||||
**Selección:** `selection`, `selection-copy`
|
||||
|
||||
**Historial:** `history`, `history-undo`, `history-redo`
|
||||
|
||||
</details>
|
||||
|
||||
Las categorías son jerárquicas: al desactivar una categoría padre (p. ej., `annotation`) se desactivan todas sus hijas.
|
||||
|
||||
### Desactivar el contador de estrellas de GitHub
|
||||
|
||||
La barra de navegación muestra un contador de estrellas de GitHub en vivo, obtenido de `api.github.com` al cargar la página. Es la única solicitud externa que no sirve a una función de PDF. Desactívalo al compilar para eliminar el código de obtención del bundle y quitar `api.github.com` de la Content-Security-Policy:
|
||||
|
||||
```bash
|
||||
docker build --build-arg DISABLE_GITHUB_STARS=true -t bentopdf .
|
||||
```
|
||||
|
||||
Las compilaciones en Simple Mode (`bentopdf-simple`) siempre omiten el contador de estrellas: este indicador solo hace falta en la versión comercial o en compilaciones completas personalizadas.
|
||||
|
||||
### URL de WASM personalizadas (aisladas de la red / autoalojadas)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Las URL de WASM se incorporan al JavaScript en **tiempo de compilación**. Los archivos WASM los descarga el **navegador del usuario** en tiempo de ejecución: Docker no los descarga durante la compilación. Para redes aisladas, debes alojar los archivos WASM en un servidor interno al que lleguen los navegadores.
|
||||
|
||||
**Flujo de trabajo completo para redes aisladas:**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Usa los códigos que muestra `bash scripts/prepare-airgap.sh --list-ocr-languages`, o busca por nombre con `bash scripts/prepare-airgap.sh --search-ocr-language <term>`, para `--ocr-languages`. Si compilas con un subconjunto restringido de OCR, pasa los mismos códigos a `VITE_TESSERACT_AVAILABLE_LANGUAGES` para que la aplicación solo muestre los idiomas incluidos. Para una salida de OCR totalmente sin conexión, aloja también el directorio incluido `ocr-fonts/` y apunta `VITE_OCR_FONT_BASE_URL` hacia él.
|
||||
|
||||
Define una variable como cadena vacía para desactivar ese módulo (los usuarios deberán configurarlo manualmente en Ajustes avanzados).
|
||||
|
||||
## Puerto personalizado
|
||||
|
||||
Por defecto, BentoPDF escucha en el puerto `8080` dentro del contenedor. Para cambiarlo, define la variable de entorno `PORT` en tiempo de ejecución:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
|
||||
```
|
||||
|
||||
| Variable | Descripción | Predeterminado |
|
||||
| -------- | ------------------------------------------- | -------------- |
|
||||
| `PORT` | Puerto de escucha de Nginx en el contenedor | `8080` |
|
||||
|
||||
Funciona tanto con el Dockerfile estándar como con el nonroot.
|
||||
|
||||
## ID de usuario personalizado (PUID/PGID)
|
||||
|
||||
Para entornos que requieren ejecutarse como un usuario concreto que no sea root (dispositivos NAS, Kubernetes con contextos de seguridad, políticas de la organización), BentoPDF ofrece un Dockerfile aparte con compatibilidad PUID/PGID al estilo LSIO.
|
||||
|
||||
### Compilar y ejecutar
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Variables de entorno
|
||||
|
||||
| Variable | Descripción | Predeterminado |
|
||||
| -------------- | --------------------------------- | -------------- |
|
||||
| `PUID` | ID de usuario con el que ejecutar | `1000` |
|
||||
| `PGID` | ID de grupo con el que ejecutar | `1000` |
|
||||
| `PORT` | Puerto de escucha de Nginx | `8080` |
|
||||
| `DISABLE_IPV6` | Desactivar el 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ómo funciona
|
||||
|
||||
El contenedor se inicia como root, crea un usuario con el PUID/PGID indicado, ajusta la propiedad de todos los directorios con permiso de escritura y después reduce privilegios con `su-exec`. El proceso de nginx se ejecuta íntegramente como el usuario que indiques.
|
||||
|
||||
> [!NOTE]
|
||||
> El `Dockerfile` estándar usa `nginx-unprivileged` (UID 101) y es el recomendado para la mayoría de los despliegues. Usa `Dockerfile.nonroot` solo cuando necesites un UID/GID concreto.
|
||||
|
||||
> [!WARNING]
|
||||
> PUID/PGID no pueden ser `0` (root). El entrypoint valida los valores y terminará con un error si son inválidos.
|
||||
|
||||
## Con Traefik (proxy inverso)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Con Caddy (proxy inverso)
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
## Límites de recursos
|
||||
|
||||
```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 (integración con systemd)
|
||||
|
||||
[Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) te permite ejecutar contenedores de Podman como servicios de systemd. Es ideal para despliegues de producción en sistemas Linux.
|
||||
|
||||
### Configuración básica de Quadlet
|
||||
|
||||
Crea un archivo de unidad de contenedor en `~/.config/containers/systemd/bentopdf.container` (usuario) o `/etc/containers/systemd/bentopdf.container` (sistema):
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Habilitar e iniciar
|
||||
|
||||
```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]
|
||||
> Para un despliegue a nivel de sistema, usa `systemctl` sin la opción `--user` y coloca el archivo en `/etc/containers/systemd/`.
|
||||
|
||||
### Quadlet con comprobación de estado
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Actualización automática con Quadlet
|
||||
|
||||
Podman puede actualizar los contenedores automáticamente cuando hay imágenes nuevas disponibles:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Configuración de red de Quadlet
|
||||
|
||||
Para una configuración de red personalizada, crea un archivo de red `bentopdf.network`:
|
||||
|
||||
```ini
|
||||
[Network]
|
||||
Subnet=10.89.0.0/24
|
||||
Gateway=10.89.0.1
|
||||
```
|
||||
|
||||
Después haz referencia a él en tu archivo de contenedor:
|
||||
|
||||
```ini
|
||||
[Container]
|
||||
Image=ghcr.io/alam00000/bentopdf-simple:latest
|
||||
ContainerName=bentopdf
|
||||
PublishPort=3000:8080
|
||||
Network=bentopdf.network
|
||||
```
|
||||
|
||||
## Actualización
|
||||
|
||||
```bash
|
||||
# Pull latest image
|
||||
docker compose pull
|
||||
|
||||
# Recreate container
|
||||
docker compose up -d
|
||||
```
|
||||
@@ -0,0 +1,266 @@
|
||||
# Desplegar en Hostinger
|
||||
|
||||
[Hostinger](https://hostinger.com) es un proveedor de alojamiento compartido muy popular. Esta guía explica cómo desplegar BentoPDF en el alojamiento compartido de Hostinger.
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- Plan de alojamiento de Hostinger con acceso al administrador de archivos
|
||||
- Node.js instalado localmente para compilar
|
||||
|
||||
## Paso 1: Compilar el proyecto
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Los archivos compilados estarán en la carpeta `dist`.
|
||||
|
||||
## Paso 2: Subir a Hostinger
|
||||
|
||||
### Despliegue en el dominio raíz
|
||||
|
||||
1. Inicia sesión en tu cuenta de Hostinger
|
||||
2. Ve a **File Manager** → **public_html**
|
||||
3. **Elimina** los archivos existentes (haz una copia de seguridad si hace falta)
|
||||
4. **Sube** todo el contenido de tu carpeta local `dist` a `public_html`
|
||||
|
||||
### Despliegue en un subdirectorio
|
||||
|
||||
Si despliegas en un subdirectorio (p. ej., `yourdomain.com/pdf-tools/`):
|
||||
|
||||
1. Compila con la URL base correcta:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf-tools/ npm run build
|
||||
```
|
||||
|
||||
2. Crea la carpeta en Hostinger:
|
||||
- Ve a **File Manager** → **public_html**
|
||||
- Crea una carpeta nueva: `pdf-tools`
|
||||
3. Sube todo el contenido de `dist` a `public_html/pdf-tools/`
|
||||
|
||||
## Paso 3: Crear el archivo .htaccess
|
||||
|
||||
Crea un archivo `.htaccess` en la raíz de tu carpeta de despliegue (`public_html` o `public_html/pdf-tools/`):
|
||||
|
||||
::: warning Importante
|
||||
Para el despliegue en un subdirectorio, cambia `RewriteBase /` por `RewriteBase /pdf-tools/` (o el nombre de tu carpeta).
|
||||
:::
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Ejemplo de .htaccess para un subdirectorio
|
||||
|
||||
Para `yourdomain.com/pdf-tools/`, actualiza estas líneas:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### Los archivos WASM no se cargan
|
||||
|
||||
Asegúrate de que los tipos MIME estén bien definidos en `.htaccess`:
|
||||
|
||||
```apache
|
||||
AddType application/wasm .wasm
|
||||
```
|
||||
|
||||
### Las herramientas de LibreOffice no funcionan
|
||||
|
||||
Las cabeceras de seguridad son críticas para SharedArrayBuffer:
|
||||
|
||||
```apache
|
||||
Header always set Cross-Origin-Opener-Policy "same-origin"
|
||||
Header always set Cross-Origin-Embedder-Policy "require-corp"
|
||||
```
|
||||
|
||||
Si las cabeceras no se aplican, contacta con el soporte de Hostinger para que habiliten `mod_headers`.
|
||||
|
||||
### Errores 404 al recargar la página
|
||||
|
||||
Asegúrate de que la regla de la extensión `.html` y las rutas de idioma estén bien configuradas. BentoPDF usa archivos HTML estáticos, no enrutamiento 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]
|
||||
```
|
||||
|
||||
### Límites de subida de archivos
|
||||
|
||||
Hostinger puede tener límites de tamaño de archivo. Sube los archivos WASM por separado si falla la subida en bloque.
|
||||
@@ -0,0 +1,538 @@
|
||||
# Guía de autoalojamiento
|
||||
|
||||
BentoPDF se puede autoalojar en tu propia infraestructura. Esta guía cubre las distintas opciones de despliegue.
|
||||
|
||||
## Inicio rápido con Docker / Podman
|
||||
|
||||
La forma más rápida de autoalojar BentoPDF:
|
||||
|
||||
> [!TIP]
|
||||
> BentoPDF se distribuye en dos compilaciones:
|
||||
>
|
||||
> - **Compilación autoalojada**: `ghcr.io/alam00000/bentopdf-simple:latest`. Todas las herramientas de PDF, **sin** el aparato de marketing (sin hero, preguntas frecuentes, testimonios ni pie de página). Úsala para despliegues internos, de equipo o de organización. **No** es una versión "lite" con menos funciones.
|
||||
> - **Compilación comercial**: `ghcr.io/alam00000/bentopdf:latest`. El sitio de marketing completo, que usa el propio bentopdf.com y los titulares de licencias comerciales que ejecutan despliegues de cara al público.
|
||||
>
|
||||
> Si dudas: descarga la compilación autoalojada.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> La conversión de archivos de Office requiere `SharedArrayBuffer`, lo que significa que la aplicación debe estar aislada entre orígenes (cross-origin isolated) y servirse desde un contexto seguro. La imagen oficial ya envía las cabeceras COOP/COEP necesarias, pero los navegadores siguen desactivando `SharedArrayBuffer` en orígenes HTTP simples de red local, como `http://192.168.x.x`.
|
||||
>
|
||||
> Usa `http://localhost` solo para pruebas en el mismo dispositivo. Si los usuarios acceden a BentoPDF mediante una IP o un nombre de host de la red local, termina la conexión con 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
|
||||
```
|
||||
|
||||
O con 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)
|
||||
|
||||
Ejecuta BentoPDF como un servicio de systemd. Crea `~/.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
|
||||
```
|
||||
|
||||
Consulta la [guía de despliegue con Docker](/es/self-hosting/docker) para ver la documentación completa de Quadlet.
|
||||
|
||||
## Compilar desde el código fuente
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Opciones de configuración
|
||||
|
||||
### Compilación autoalojada (Modo Simple)
|
||||
|
||||
La compilación autoalojada (la imagen `bentopdf-simple`, también llamada Modo Simple) es **funcionalmente idéntica** a la compilación comercial: todas las herramientas de PDF están presentes y se comportan igual. Simplemente oculta el aparato de marketing que solo tiene sentido en el propio bentopdf.com o en un despliegue comercial de cara al público. **No es una versión "lite" ni con menos funciones.**
|
||||
|
||||
**Qué oculta la compilación autoalojada** (solo cosmético, no se elimina ninguna función de PDF):
|
||||
|
||||
- Barra de navegación, sección hero, sección de características, preguntas frecuentes, testimonios, pie de página
|
||||
- Cambia el título de la página a "Herramientas PDF"
|
||||
|
||||
**Qué conserva la compilación autoalojada** (todo lo que realmente trabaja con PDF):
|
||||
|
||||
- Todas las herramientas de PDF (fusionar, dividir, editar, firmar, OCR, conversión de Office, etc.)
|
||||
- Compatibilidad con marca personalizada y toda la configuración de compilación y de tiempo de ejecución
|
||||
|
||||
La compilación comercial (`ghcr.io/alam00000/bentopdf:latest`) es la que impulsa el propio bentopdf.com y la usan los titulares de licencias comerciales que ejecutan despliegues de cara al público. Añade el hero, las preguntas frecuentes, los testimonios y el pie de página, que no tendrían sentido en una herramienta interna.
|
||||
|
||||
Si autoalojas BentoPDF para tu equipo, tu organización o como herramienta interna, descarga la compilación autoalojada:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Consulta [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md) para ver todos los detalles.
|
||||
|
||||
### Compilación comercial (despliegues de cara al público con tu propia marca)
|
||||
|
||||
La compilación comercial (la imagen `bentopdf`, sin el sufijo `-simple`) es la que impulsa el propio bentopdf.com. Incluye el sitio de marketing completo (hero, características, preguntas frecuentes, testimonios, pie de página) además de todas las herramientas de PDF. Usa esta compilación cuando quieras **desplegar BentoPDF como un servicio de PDF de cara al público bajo tu propia marca**, por ejemplo:
|
||||
|
||||
- Ejecutas BentoPDF como un SaaS alojado para usuarios finales en tu propio dominio
|
||||
- Quieres la experiencia de la página de inicio (secciones de marketing y herramientas), no solo la superficie básica de herramientas
|
||||
- Eres titular de una licencia comercial e integras BentoPDF en un producto comercial
|
||||
|
||||
**Ejecútala tal cual** (con la marca BentoPDF, útil para previsualizar la compilación):
|
||||
|
||||
```bash
|
||||
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
|
||||
```
|
||||
|
||||
**Compila con tu propia marca** (el flujo comercial típico):
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Todas las demás opciones de compilación (`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, sustitución de URL de WASM, etc.) funcionan igual que en la compilación autoalojada.
|
||||
|
||||
::: warning Licencias
|
||||
Ejecutar la compilación comercial está permitido bajo cualquiera de las dos opciones de licencia de BentoPDF:
|
||||
|
||||
- **AGPL-3.0** (gratuita): permitida si tu despliegue publica todo su código fuente bajo AGPL, incluidas las modificaciones de marca y la lógica de negocio asociada.
|
||||
- **Licencia comercial** (79 $ de por vida): necesaria para despliegues de código cerrado o propietarios en los que no publicas tu fork con la marca ni tu código de negocio.
|
||||
|
||||
Consulta la [página de licencias](https://www.bentopdf.com/licensing) para ver la comparación completa. Los módulos WASM con licencia AGPL (PyMuPDF, Ghostscript, CoherentPDF) se cargan desde una CDN en tiempo de ejecución, por lo que no entran en tu imagen y no cambian tu situación de licencias.
|
||||
:::
|
||||
|
||||
### URL base
|
||||
|
||||
Despliega en un subdirectorio:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf-tools/ npm run build
|
||||
```
|
||||
|
||||
### Marca personalizada
|
||||
|
||||
Sustituye el logotipo, el nombre y el texto del pie de página de BentoPDF por los tuyos en tiempo de compilación:
|
||||
|
||||
| Variable | Descripción | Valor por defecto |
|
||||
| ------------------ | ------------------------------------------------------------ | --------------------------------------- |
|
||||
| `VITE_BRAND_NAME` | Nombre de marca que se muestra en el encabezado y el pie | `BentoPDF` |
|
||||
| `VITE_BRAND_LOGO` | Ruta del logotipo relativa a `public/` | `images/favicon-no-bg.svg` |
|
||||
| `VITE_FOOTER_TEXT` | Texto personalizado del pie de página y de derechos de autor | `© 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
|
||||
```
|
||||
|
||||
O mediante 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 .
|
||||
```
|
||||
|
||||
La marca personalizada funciona tanto en el modo completo como en el Modo Simple, y se puede combinar con todas las demás opciones de compilación (`BASE_URL`, `SIMPLE_MODE`, `VITE_DEFAULT_LANGUAGE`).
|
||||
|
||||
### Desactivar herramientas específicas
|
||||
|
||||
Oculta herramientas concretas por motivos de cumplimiento o seguridad. Las herramientas desactivadas se eliminan de la página de inicio, la búsqueda, los atajos, el generador de flujos de trabajo y el acceso directo por URL.
|
||||
|
||||
Los ID de las herramientas son la URL de la página sin `.html`: abre cualquier herramienta y mira la URL (p. ej., `edit-pdf`, `sign-pdf`, `encrypt-pdf`).
|
||||
|
||||
**En tiempo de compilación** (incluido en el bundle):
|
||||
|
||||
```bash
|
||||
DISABLE_TOOLS="edit-pdf,sign-pdf" npm run build
|
||||
```
|
||||
|
||||
**En tiempo de ejecución** (sin recompilar: monta un `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
|
||||
```
|
||||
|
||||
Se pueden combinar ambos métodos: las listas se fusionan.
|
||||
|
||||
También puedes desactivar funciones específicas dentro del Editor de PDF (p. ej., la redacción) sin desactivar la herramienta completa, añadiendo `editorDisabledCategories` a `config.json`. Consulta la [guía de Docker](/es/self-hosting/docker#desactivar-funciones-del-editor) para ver la lista completa de categorías.
|
||||
|
||||
## Guías de despliegue
|
||||
|
||||
Elige tu plataforma:
|
||||
|
||||
- [Vercel](/es/self-hosting/vercel)
|
||||
- [Netlify](/es/self-hosting/netlify)
|
||||
- [Cloudflare Pages](/es/self-hosting/cloudflare)
|
||||
- [AWS S3 + CloudFront](/es/self-hosting/aws)
|
||||
- [Hostinger](/es/self-hosting/hostinger)
|
||||
- [Nginx](/es/self-hosting/nginx)
|
||||
- [Apache](/es/self-hosting/apache)
|
||||
- [Docker](/es/self-hosting/docker)
|
||||
- [Kubernetes](/es/self-hosting/kubernetes)
|
||||
- [Proxy CORS](/es/self-hosting/cors-proxy): necesario para las firmas digitales
|
||||
|
||||
## Problemas comunes
|
||||
|
||||
### La conversión de Word / ODT / Excel / PowerPoint a PDF se cuelga (SharedArrayBuffer no disponible)
|
||||
|
||||
**Síntoma**: las conversiones de documentos basadas en LibreOffice (Word, ODT, Excel, PowerPoint a PDF) se quedan colgadas en torno al 55% o no llegan a iniciarse. La consola del navegador puede mostrar:
|
||||
|
||||
> `ReferenceError: SharedArrayBuffer is not defined`
|
||||
|
||||
...o `window.crossOriginIsolated` devuelve `false`, o la compilación de WASM informa `expected magic word 00 61 73 6d, found 1f 8b 08 08`.
|
||||
|
||||
**Causa**: LibreOffice WASM requiere `SharedArrayBuffer`, que el navegador solo activa cuando la página está **aislada entre orígenes (cross-origin isolated)** Y se sirve desde un **contexto seguro**. Eso significa que deben cumplirse dos cosas:
|
||||
|
||||
1. Todas las respuestas incluyen ambas cabeceras:
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
2. La página se sirve desde `https://...` o `http://localhost`. HTTP simple sobre una IP de red local (p. ej. `http://192.168.x.x`) NO cuenta como seguro: los navegadores desactivan `SharedArrayBuffer` allí.
|
||||
|
||||
La discrepancia `00 61 73 6d / 1f 8b 08 08` es un subsíntoma distinto: a los archivos precomprimidos `.wasm.gz` / `.data.gz` les falta la cabecera de respuesta `Content-Encoding: gzip`, por lo que el navegador recibe bytes gzip sin procesar en lugar de WASM descomprimido.
|
||||
|
||||
**Solución**: consulta la guía de despliegue de tu plataforma para ver la configuración exacta:
|
||||
|
||||
- [Nginx →](/es/self-hosting/nginx#la-conversion-de-word-odt-excel-a-pdf-no-funciona)
|
||||
- [Apache →](/es/self-hosting/apache#word-odt-excel-a-pdf-no-funciona)
|
||||
- [AWS S3 + CloudFront →](/es/self-hosting/aws#paso-3b-politica-de-cabeceras-de-respuesta-necesaria-para-libreoffice-wasm)
|
||||
- [Cloudflare Pages →](/es/self-hosting/cloudflare#archivo-de-configuracion) (archivo `_headers`)
|
||||
- [Netlify →](/es/self-hosting/netlify#la-conversion-de-word-odt-excel-a-pdf-se-queda-atascada-en-el-55) (`netlify.toml`)
|
||||
- [Vercel →](/es/self-hosting/vercel#la-conversion-de-word-odt-excel-a-pdf-no-funciona) (`vercel.json`)
|
||||
- [Hostinger →](/es/self-hosting/hostinger#las-herramientas-de-libreoffice-no-funcionan) (`.htaccess`)
|
||||
- [Kubernetes →](/es/self-hosting/kubernetes#como-comprobar-que-las-cabeceras-de-sharedarraybuffer-siguen-funcionando-ingress-gateway)
|
||||
- **Docker**: lo gestiona automáticamente la configuración de nginx incluida, no hace falta hacer nada.
|
||||
|
||||
**Verificación**: abre la consola de DevTools en cualquier página de BentoPDF y ejecuta:
|
||||
|
||||
```js
|
||||
console.log(window.crossOriginIsolated); // should be true
|
||||
console.log(typeof SharedArrayBuffer); // should be "function"
|
||||
```
|
||||
|
||||
Si la página usa HTTPS o `http://localhost` Y ambas cabeceras COEP/COOP están presentes en todas las respuestas, ambas comprobaciones se superan. Si estás en `http://192.168.x.x` u otro origen HTTP que no sea de loopback, termina la conexión con HTTPS: no hay una solución que dependa solo de cabeceras.
|
||||
|
||||
### Archivos `.mjs` servidos como `application/octet-stream`
|
||||
|
||||
**Síntoma**: Firmar PDF / Rellenar formularios / otras herramientas muestran un visor en blanco o no se cargan. La consola del navegador informa:
|
||||
|
||||
> `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.`
|
||||
|
||||
**Causa**: tu servidor web o proxy inverso no tiene una asignación de tipo MIME para los archivos `.mjs` (el visor de PDF incluido incorpora módulos ES con esa extensión). Muchas configuraciones de servidor por defecto usan `application/octet-stream` para las extensiones desconocidas, y los navegadores se niegan a ejecutarlas como módulos ES.
|
||||
|
||||
**Solución**: consulta la guía de despliegue de tu plataforma para ver el fragmento exacto:
|
||||
|
||||
- [Nginx →](/es/self-hosting/nginx#firmar-pdf-o-rellenar-formularios-muestra-un-visor-en-blanco-error-mime-de-mjs)
|
||||
- [Apache →](/es/self-hosting/apache#firmar-pdf-o-el-rellenador-de-formularios-muestra-un-visor-en-blanco-error-mime-de-mjs)
|
||||
- [AWS S3 + CloudFront →](/es/self-hosting/aws#paso-2-compilar-y-subir) (consulta `aws s3 cp ... --include "*.mjs"`)
|
||||
- [Kubernetes →](/es/self-hosting/kubernetes#errores-de-tipo-mime-de-mjs-iframe-en-blanco-en-firmar-pdf-rellenar-formularios)
|
||||
- **Docker, Vercel, Netlify, Cloudflare Pages, Hostinger**: se gestiona automáticamente, no hace falta hacer nada.
|
||||
|
||||
**Verificación**: abre DevTools → pestaña Network, busca la petición `.mjs` que falla y revisa la cabecera de respuesta `Content-Type`. Debería ser `application/javascript`. Si sigue siendo `application/octet-stream`, puede que un proxy inverso o una CDN externos estén volviendo a detectar el tipo: revisa cada capa de tu cadena de servicio.
|
||||
|
||||
## Configuración de WASM (componentes AGPL)
|
||||
|
||||
BentoPDF **no incluye** en su código fuente las bibliotecas de procesamiento con licencia AGPL, pero **preconfigura las URL de CDN** para que todas las funciones funcionen de inmediato, sin configuración manual.
|
||||
|
||||
::: tip Cero configuración por defecto
|
||||
Desde la v2.0.0, los módulos WASM están preconfigurados para cargarse desde la CDN de jsDelivr mediante variables de entorno. Todas las funciones avanzadas funcionan de inmediato sin ninguna configuración del usuario.
|
||||
:::
|
||||
|
||||
| Componente | Licencia | Funciones |
|
||||
| --------------- | -------- | ----------------------------------------------------------------------------- |
|
||||
| **PyMuPDF** | AGPL-3.0 | Conversión de EPUB/MOBI/FB2/XPS, extracción de imágenes, extracción de tablas |
|
||||
| **Ghostscript** | AGPL-3.0 | Conversión a PDF/A, compresión, corrección de inclinación, rasterización |
|
||||
| **CoherentPDF** | AGPL-3.0 | Tabla de contenido, adjuntos, fusión de PDF con marcadores |
|
||||
|
||||
### Variables de entorno por defecto
|
||||
|
||||
Se definen en `.env.production` y se incluyen en la compilación:
|
||||
|
||||
```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=
|
||||
```
|
||||
|
||||
### Sustituir las URL de WASM
|
||||
|
||||
Puedes sustituir los valores por defecto en tiempo de compilación para despliegues personalizados:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Para desactivar un módulo por completo (requiere configuración manual del usuario en Ajustes avanzados), asigna una cadena vacía a su variable.
|
||||
|
||||
Para OCR, deja vacías todas las variables `VITE_TESSERACT_*` y mantén los recursos en línea por defecto, o define juntas las URL de worker, core e idiomas para un OCR autoalojado o sin conexión. Si incluyes solo idiomas de OCR concretos, define también `VITE_TESSERACT_AVAILABLE_LANGUAGES` con los mismos códigos separados por comas para que la interfaz solo ofrezca los idiomas instalados y las selecciones no admitidas fallen con un error descriptivo. Para obtener PDF con capa de texto totalmente sin conexión, define también `VITE_OCR_FONT_BASE_URL` con el directorio interno que sirve las fuentes de OCR incluidas.
|
||||
|
||||
Los usuarios también pueden sustituir estos valores por defecto en cualquier momento mediante **Ajustes avanzados** en la interfaz: las sustituciones del usuario guardadas en el navegador tienen prioridad sobre los valores por defecto del entorno.
|
||||
|
||||
### Despliegue aislado de la red / sin conexión
|
||||
|
||||
Para redes sin acceso a internet (gobierno, sanidad, finanzas, etc.). Las URL de WASM se incluyen en el JavaScript en **tiempo de compilación**: los archivos WASM reales los descarga el **navegador del usuario** en tiempo de ejecución. Por eso debes preparar todo en una máquina con internet y luego transferirlo a la red aislada.
|
||||
|
||||
#### Script automatizado (recomendado)
|
||||
|
||||
El script incluido `prepare-airgap.sh` automatiza todo el proceso: descarga los paquetes WASM, compila la imagen de Docker y genera un paquete autocontenido con un script de configuración.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Esto genera un directorio con el paquete:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
Transfiere el paquete a la red aislada mediante USB, un repositorio interno de artefactos o un método aprobado. Después ejecuta el script de configuración incluido:
|
||||
|
||||
```bash
|
||||
cd bentopdf-airgap-bundle
|
||||
bash setup.sh
|
||||
```
|
||||
|
||||
El script de configuración carga la imagen de Docker, extrae los archivos WASM y, opcionalmente, inicia el contenedor.
|
||||
|
||||
**Opciones del script:**
|
||||
|
||||
| Opción | Descripción | Valor por defecto |
|
||||
| ------------------------------ | ----------------------------------------------------------- | ------------------------------------- |
|
||||
| `--wasm-base-url <url>` | Dónde se alojarán los WASM internamente | _(obligatorio, se pregunta si falta)_ |
|
||||
| `--image-name <name>` | Etiqueta de la imagen de Docker | `bentopdf` |
|
||||
| `--output-dir <path>` | Directorio de salida del paquete | `./bentopdf-airgap-bundle` |
|
||||
| `--simple-mode` | Activa el Modo Simple | desactivado |
|
||||
| `--base-url <path>` | URL base de subdirectorio (p. ej. `/pdf/`) | `/` |
|
||||
| `--language <code>` | Idioma predeterminado de la interfaz (p. ej. `fr`, `de`) | _(ninguno)_ |
|
||||
| `--brand-name <name>` | Nombre de marca personalizado | _(ninguno)_ |
|
||||
| `--brand-logo <path>` | Ruta del logotipo relativa a `public/` | _(ninguno)_ |
|
||||
| `--footer-text <text>` | Texto personalizado del pie de página | _(ninguno)_ |
|
||||
| `--ocr-languages <list>` | Idiomas de OCR separados por comas que se incluirán | `eng` |
|
||||
| `--list-ocr-languages` | Muestra los códigos y nombres de OCR admitidos y sale | desactivado |
|
||||
| `--search-ocr-language <term>` | Busca códigos de OCR por nombre o abreviatura | desactivado |
|
||||
| `--dockerfile <path>` | Dockerfile que se usará | `Dockerfile` |
|
||||
| `--skip-docker` | Omite la compilación y exportación de Docker | desactivado |
|
||||
| `--skip-wasm` | Omite la descarga de WASM (reutiliza los `.tgz` existentes) | desactivado |
|
||||
|
||||
El prompt interactivo también acepta `list` para mostrar la lista completa de códigos de Tesseract admitidos y `search <term>` para encontrar coincidencias como `search german` o `search chi`.
|
||||
|
||||
::: warning Requisito de mismo origen
|
||||
Los archivos WASM deben servirse desde el **mismo origen** que la aplicación BentoPDF. Los Web Workers usan `importScripts()`, que no puede cargar scripts de otro origen. Por ejemplo, si BentoPDF se ejecuta en `https://internal.example.com`, la URL base de WASM también debe ser `https://internal.example.com/wasm`.
|
||||
:::
|
||||
|
||||
#### Pasos manuales
|
||||
|
||||
<details>
|
||||
<summary>Si prefieres hacerlo manualmente sin el script</summary>
|
||||
|
||||
**Paso 1: Descargar los paquetes de WASM y OCR** (en una máquina con 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
|
||||
```
|
||||
|
||||
**Paso 2: Compilar la imagen de Docker con URL internas**
|
||||
|
||||
```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 .
|
||||
```
|
||||
|
||||
**Paso 3: Exportar la imagen de Docker**
|
||||
|
||||
```bash
|
||||
docker save bentopdf -o bentopdf.tar
|
||||
```
|
||||
|
||||
**Paso 4: Transferir a la red aislada**
|
||||
|
||||
Copia mediante USB, un repositorio interno de artefactos o un método de transferencia aprobado:
|
||||
|
||||
- `bentopdf.tar`: la imagen de Docker
|
||||
- Los cinco paquetes `.tgz` de WASM/OCR del Paso 1
|
||||
- El directorio `tesseract-langdata/` del Paso 1
|
||||
- El directorio `ocr-fonts/` del Paso 1
|
||||
|
||||
**Paso 5: Configurar dentro de la red aislada**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Asegúrate de que los archivos sean accesibles en las URL que configuraste en el Paso 2, incluidas `.../ocr/worker.min.js`, `.../ocr/core`, `.../ocr/lang-data` y `.../ocr/fonts`.
|
||||
|
||||
</details>
|
||||
|
||||
::: info ¿Compilas desde el código fuente en lugar de usar Docker?
|
||||
Define las variables en `.env.production` antes de ejecutar `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
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Alojar tu propio proxy de WASM
|
||||
|
||||
Si necesitas servir archivos WASM con licencia AGPL con las cabeceras CORS adecuadas, puedes desplegar un proxy sencillo. Consulta la [guía del proxy de WASM en Cloudflare](https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md) para ver una implementación de ejemplo.
|
||||
|
||||
::: tip ¿Por qué por separado?
|
||||
Esta separación garantiza:
|
||||
|
||||
- Un cumplimiento legal claro para los usuarios comerciales
|
||||
- Que el núcleo de BentoPDF siga bajo su doble licencia (AGPL-3.0 / Comercial)
|
||||
- Que los archivos WASM se carguen en tiempo de ejecución, no incluidos en el código fuente
|
||||
:::
|
||||
|
||||
## Requisitos del sistema
|
||||
|
||||
| Requisito | Mínimo |
|
||||
| -------------- | --------------------------------- |
|
||||
| Almacenamiento | ~100 MB (núcleo sin módulos AGPL) |
|
||||
| RAM | 512 MB |
|
||||
| CPU | Cualquier procesador moderno |
|
||||
|
||||
::: tip
|
||||
BentoPDF es un sitio estático: no se necesita base de datos ni servidor backend.
|
||||
:::
|
||||
@@ -0,0 +1,211 @@
|
||||
# Despliegue con Kubernetes
|
||||
|
||||
Kubernetes puede ser excesivo para un sitio estático, pero puede encajar muy bien si ya estandarizas con Helm + GitOps.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Cabeceras necesarias para la conversión de archivos de Office**
|
||||
>
|
||||
> Las herramientas basadas en LibreOffice (conversión de Word, Excel y PowerPoint) requieren estas cabeceras HTTP para la compatibilidad con `SharedArrayBuffer`:
|
||||
>
|
||||
> - `Cross-Origin-Opener-Policy: same-origin`
|
||||
> - `Cross-Origin-Embedder-Policy: require-corp`
|
||||
>
|
||||
> Las imágenes oficiales de BentoPDF con nginx incluyen estas cabeceras. En Kubernetes, **los controladores de Ingress/Gateway también son proxies inversos**, así que asegúrate de que estas cabeceras se conserven (o añádelas en el borde).
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- Clúster de Kubernetes
|
||||
- Helm v3
|
||||
- Una imagen de BentoPDF con nginx (p. ej. `ghcr.io/alam00000/bentopdf:<tag>`) que sirva en el **puerto 8080**
|
||||
|
||||
## Despliegue con Helm
|
||||
|
||||
### Instalar desde este repositorio (chart local)
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Instalar desde GHCR (chart OCI)
|
||||
|
||||
Si el chart está publicado en GHCR como artefacto 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
|
||||
```
|
||||
|
||||
## Exponerlo
|
||||
|
||||
### Port-forward (prueba rápida)
|
||||
|
||||
```bash
|
||||
kubectl -n bentopdf port-forward deploy/bentopdf 8080:8080
|
||||
```
|
||||
|
||||
### Ingress (opcional)
|
||||
|
||||
Activa Ingress (ejemplo para nginx-ingress):
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: pdf.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
```
|
||||
|
||||
### Gateway API (opcional)
|
||||
|
||||
Este chart es compatible con `Gateway` + `HTTPRoute` de Gateway API.
|
||||
|
||||
Ejemplo (operador de Gateway API de Cloudflare):
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Cómo comprobar que las cabeceras de SharedArrayBuffer siguen funcionando (Ingress/Gateway)
|
||||
|
||||
### Lo que "debería" pasar
|
||||
|
||||
La configuración de nginx de BentoPDF establece las cabeceras de respuesta necesarias. La mayoría de los controladores de Ingress/Gateway **pasan las cabeceras de respuesta del upstream sin modificarlas**.
|
||||
|
||||
### Qué puede romperlo
|
||||
|
||||
- Una política del controlador o del borde que **sobrescriba** o **elimine** cabeceras de respuesta
|
||||
- Un middleware de "cabeceras de seguridad" que establezca valores de COOP/COEP distintos
|
||||
|
||||
### Cómo verificarlo
|
||||
|
||||
Ejecuta esto contra tu endpoint público:
|
||||
|
||||
```bash
|
||||
curl -I https://pdf.example.com/ | egrep -i 'cross-origin-opener-policy|cross-origin-embedder-policy'
|
||||
```
|
||||
|
||||
Deberías ver:
|
||||
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
|
||||
### Si tu controlador de Ingress no las conserva
|
||||
|
||||
Añade las cabeceras en el borde (depende del controlador). Ejemplo para **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;
|
||||
```
|
||||
|
||||
## Errores de tipo MIME de `.mjs` (iframe en blanco en Firmar PDF / Rellenar formularios)
|
||||
|
||||
Si la consola del navegador muestra `Failed to load module script: ... non-JavaScript MIME type "application/octet-stream"` en una petición `.mjs`, un controlador de Ingress/Gateway situado delante del nginx de BentoPDF está reemplazando la cabecera `Content-Type` del upstream por `application/octet-stream`.
|
||||
|
||||
El nginx de la imagen de BentoPDF ya sirve `.mjs` como `application/javascript`. La solución es evitar que el controlador la elimine o la reemplace. Para nginx-ingress:
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
location ~* \.mjs$ {
|
||||
types {} default_type application/javascript;
|
||||
}
|
||||
```
|
||||
|
||||
O bien usa un filtro `ResponseHeaderModifier` en Gateway API para establecer `Content-Type: application/javascript` en las rutas `.mjs`. Verifica con DevTools → pestaña Network: la petición `.mjs` que fallaba ahora debería mostrar `content-type: application/javascript`.
|
||||
|
||||
### Si usas Gateway API y quieres forzar la adición de cabeceras
|
||||
|
||||
Gateway API admite un filtro `ResponseHeaderModifier`. Puedes asociarlo en `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
|
||||
```
|
||||
|
||||
La compatibilidad con filtros concretos depende de tu controlador de Gateway; si se ignora un filtro, añade las cabeceras en la capa del borde o del controlador.
|
||||
|
||||
## Desactivar herramientas específicas
|
||||
|
||||
Usa un ConfigMap para desactivar herramientas en tiempo de ejecución sin reconstruir la imagen:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bentopdf-config
|
||||
namespace: bentopdf
|
||||
data:
|
||||
config.json: |
|
||||
{
|
||||
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
|
||||
}
|
||||
```
|
||||
|
||||
Móntalo en el directorio servido:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Los ID de las herramientas son la URL de la página sin `.html`: abre cualquier herramienta y mira la URL (p. ej., `edit-pdf`, `merge-pdf`, `compress-pdf`). Las herramientas desactivadas se ocultan en la página de inicio, la búsqueda, los atajos, el generador de flujos de trabajo y el acceso directo por URL. Consulta la [guía de Docker](/es/self-hosting/docker#desactivar-herramientas-concretas) para ver la lista completa de opciones.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Despliegue en Netlify
|
||||
|
||||
[Netlify](https://netlify.com) ofrece un excelente alojamiento de sitios estáticos con un generoso plan gratuito.
|
||||
|
||||
## Despliegue con un clic
|
||||
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/alam00000/bentopdf)
|
||||
|
||||
## Despliegue manual
|
||||
|
||||
### Paso 1: Conectar el repositorio
|
||||
|
||||
1. Inicia sesión en [Netlify](https://app.netlify.com)
|
||||
2. Haz clic en "Add new site" → "Import an existing project"
|
||||
3. Conecta tu cuenta de GitHub
|
||||
4. Selecciona tu fork de BentoPDF
|
||||
|
||||
### Paso 2: Configurar los ajustes de compilación
|
||||
|
||||
| Ajuste | Valor |
|
||||
| ------------------------- | --------------- |
|
||||
| Comando de compilación | `npm run build` |
|
||||
| Directorio de publicación | `dist` |
|
||||
| Versión de Node | 18+ |
|
||||
|
||||
### Paso 3: Desplegar
|
||||
|
||||
Haz clic en "Deploy site" y espera a que termine la compilación.
|
||||
|
||||
## Archivo de configuración
|
||||
|
||||
Crea `netlify.toml` en la raíz de tu proyecto:
|
||||
|
||||
```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 Importante
|
||||
Las cabeceras `Cross-Origin-Embedder-Policy` y `Cross-Origin-Opener-Policy` son necesarias para las conversiones de Word/ODT/Excel/PowerPoint a PDF. Sin ellas, `SharedArrayBuffer` no está disponible y el motor LibreOffice WASM no podrá inicializarse.
|
||||
|
||||
Las cabeceras `Content-Encoding: gzip` en los archivos `.wasm.gz` y `.data.gz` indican al navegador que los descomprima automáticamente. Sin ellas, el navegador recibe bytes gzip sin procesar y la compilación de WASM falla.
|
||||
:::
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
Configúralas en Site settings → Environment variables:
|
||||
|
||||
| Variable | Descripción |
|
||||
| ----------------------- | -------------------------------------------------------------------- |
|
||||
| `SIMPLE_MODE` | Ponla en `true` para una compilación mínima |
|
||||
| `VITE_BRAND_NAME` | Nombre de marca personalizado (reemplaza "BentoPDF") |
|
||||
| `VITE_BRAND_LOGO` | Ruta del logotipo relativa a `public/` (p. ej. `images/my-logo.svg`) |
|
||||
| `VITE_FOOTER_TEXT` | Texto personalizado del pie de página y de derechos de autor |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Idioma predeterminado de la interfaz (p. ej. `fr`, `de`, `es`) |
|
||||
|
||||
## Dominio personalizado
|
||||
|
||||
1. Ve a Site settings → Domain management
|
||||
2. Haz clic en "Add custom domain"
|
||||
3. Sigue las instrucciones de configuración de DNS
|
||||
|
||||
## Archivos multimedia grandes
|
||||
|
||||
Para archivos WASM grandes, considera activar [Netlify Large Media](https://docs.netlify.com/large-media/overview/):
|
||||
|
||||
```bash
|
||||
netlify lm:setup
|
||||
git lfs track "*.wasm"
|
||||
```
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### La conversión de Word/ODT/Excel a PDF se queda atascada en el 55%
|
||||
|
||||
Si las conversiones de documentos se quedan colgadas en el 55%, abre la consola de DevTools y comprueba:
|
||||
|
||||
```js
|
||||
console.log(window.crossOriginIsolated); // should be true
|
||||
console.log(typeof SharedArrayBuffer); // should be "function"
|
||||
```
|
||||
|
||||
Si `crossOriginIsolated` es `false`, las cabeceras COEP/COOP de tu `netlify.toml` no se están aplicando. Asegúrate de que el archivo esté en la raíz de tu proyecto y vuelve a desplegar.
|
||||
|
||||
Si ves `expected magic word 00 61 73 6d, found 1f 8b 08 08` en la consola, a los archivos `.wasm.gz` les faltan las cabeceras `Content-Encoding: gzip`. Asegúrate de que los bloques `[[headers]]` de `soffice.wasm.gz` y `soffice.data.gz` estén en tu `netlify.toml`.
|
||||
|
||||
### Falla la compilación
|
||||
|
||||
Comprueba la compatibilidad de la versión de Node:
|
||||
|
||||
```toml
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
```
|
||||
|
||||
### Carga inicial lenta
|
||||
|
||||
Activa la optimización de recursos en Site settings → Build & deploy → Asset optimization.
|
||||
@@ -0,0 +1,232 @@
|
||||
# Despliegue con Nginx
|
||||
|
||||
Aloja BentoPDF en tu propio servidor con Nginx.
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- Servidor Ubuntu/Debian
|
||||
- Nginx instalado
|
||||
- Certificado SSL (recomendado: Let's Encrypt)
|
||||
|
||||
## Paso 1: Compilar el proyecto
|
||||
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Para personalizar la marca, define variables de entorno antes de compilar:
|
||||
|
||||
```bash
|
||||
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
|
||||
```
|
||||
|
||||
## Paso 2: Copiar los archivos
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/bentopdf
|
||||
sudo cp -r dist/* /var/www/bentopdf/
|
||||
sudo chown -R www-data:www-data /var/www/bentopdf
|
||||
```
|
||||
|
||||
## Paso 3: Configuración de Nginx
|
||||
|
||||
Crea `/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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Paso 4: Activar el sitio
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Paso 5: SSL con Let's Encrypt
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
## Despliegue en un subdirectorio
|
||||
|
||||
Para alojarlo en `/pdf/`:
|
||||
|
||||
```nginx
|
||||
location /pdf/ {
|
||||
alias /var/www/bentopdf/;
|
||||
try_files $uri $uri/ /pdf/index.html;
|
||||
}
|
||||
```
|
||||
|
||||
Compila con:
|
||||
|
||||
```bash
|
||||
BASE_URL=/pdf/ npm run build
|
||||
```
|
||||
|
||||
## Ajuste del rendimiento
|
||||
|
||||
Añade a `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;
|
||||
}
|
||||
```
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### WASM no se carga
|
||||
|
||||
Asegúrate de que el tipo MIME esté definido:
|
||||
|
||||
```nginx
|
||||
types {
|
||||
application/wasm wasm;
|
||||
}
|
||||
```
|
||||
|
||||
### Firmar PDF o Rellenar formularios muestra un visor en blanco (error MIME de `.mjs`)
|
||||
|
||||
Si la consola del navegador muestra:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
...y la petición que falla termina en `.mjs`, a tu nginx le falta la asignación `.mjs → application/javascript`. El visor de PDF incluido (usado por Firmar PDF, Rellenar formularios y algunas otras herramientas) incorpora archivos de módulos ES con extensión `.mjs`, y el `mime.types` por defecto de nginx no los incluye.
|
||||
|
||||
Para solucionarlo, añade `mjs` a la línea `application/javascript` existente en `/etc/nginx/mime.types`:
|
||||
|
||||
```nginx
|
||||
application/javascript js mjs;
|
||||
```
|
||||
|
||||
> NO añadas una _segunda_ línea `application/javascript`: nginx analiza `mime.types` línea por línea y solo prevalece una asignación de tipo a extensión, así que en algunas versiones la segunda línea se ignora sin avisar. Edita la línea existente para añadir `mjs` junto a `js`.
|
||||
|
||||
O bien sobrescríbelo localmente dentro de tu bloque server (sin editar el archivo global):
|
||||
|
||||
```nginx
|
||||
types {
|
||||
application/javascript js mjs;
|
||||
application/wasm wasm;
|
||||
}
|
||||
```
|
||||
|
||||
Recarga nginx después del cambio:
|
||||
|
||||
```bash
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Para confirmarlo: en DevTools del navegador → pestaña Network, busca la petición `.mjs` que fallaba y revisa la cabecera de respuesta `Content-Type`. Ahora debería ser `application/javascript`, no `application/octet-stream`.
|
||||
|
||||
### La conversión de Word/ODT/Excel a PDF no funciona
|
||||
|
||||
LibreOffice WASM requiere `SharedArrayBuffer`, que necesita las cabeceras `Cross-Origin-Embedder-Policy` y `Cross-Origin-Opener-Policy`. También necesita un contexto seguro, así que `http://localhost` sirve para pruebas locales, pero `http://192.168.x.x` u otras IP de la red local normalmente requieren HTTPS. Ten en cuenta que las directivas `add_header` de nginx dentro de un bloque `location` **sobrescriben** las directivas `add_header` del nivel de servidor: no se combinan. Todo bloque `location` con sus propios `add_header` debe incluir las cabeceras COEP/COOP.
|
||||
|
||||
Verifica con:
|
||||
|
||||
```bash
|
||||
curl -I https://your-domain.com/ | grep -i cross-origin
|
||||
```
|
||||
|
||||
Si usas un proxy inverso delante de nginx, asegúrate de que conserve estas cabeceras.
|
||||
|
||||
### 502 Bad Gateway
|
||||
|
||||
Revisa los registros de errores de Nginx:
|
||||
|
||||
```bash
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
# Despliegue en Vercel
|
||||
|
||||
[Vercel](https://vercel.com) ofrece la experiencia de despliegue más sencilla para sitios estáticos.
|
||||
|
||||
## Despliegue con un clic
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/alam00000/bentopdf)
|
||||
|
||||
## Despliegue manual
|
||||
|
||||
### Paso 1: Hacer un fork del repositorio
|
||||
|
||||
Haz un fork de [bentopdf/bentopdf](https://github.com/alam00000/bentopdf) en tu cuenta de GitHub.
|
||||
|
||||
### Paso 2: Importar en Vercel
|
||||
|
||||
1. Ve a [vercel.com/new](https://vercel.com/new)
|
||||
2. Selecciona tu repositorio con el fork
|
||||
3. Configura el proyecto:
|
||||
|
||||
| Ajuste | Valor |
|
||||
| ---------------------- | --------------- |
|
||||
| Framework Preset | Vite |
|
||||
| Comando de compilación | `npm run build` |
|
||||
| Directorio de salida | `dist` |
|
||||
| Comando de instalación | `npm install` |
|
||||
|
||||
### Paso 3: Variables de entorno (opcional)
|
||||
|
||||
Añádelas si las necesitas:
|
||||
|
||||
| Variable | Descripción |
|
||||
| ----------------------- | -------------------------------------------------------------------- |
|
||||
| `SIMPLE_MODE` | Ponla en `true` para una interfaz mínima |
|
||||
| `VITE_BRAND_NAME` | Nombre de marca personalizado (reemplaza "BentoPDF") |
|
||||
| `VITE_BRAND_LOGO` | Ruta del logotipo relativa a `public/` (p. ej. `images/my-logo.svg`) |
|
||||
| `VITE_FOOTER_TEXT` | Texto personalizado del pie de página y de derechos de autor |
|
||||
| `VITE_DEFAULT_LANGUAGE` | Idioma predeterminado de la interfaz (p. ej. `fr`, `de`, `es`) |
|
||||
|
||||
### Paso 4: Desplegar
|
||||
|
||||
Haz clic en "Deploy" y espera a que termine la compilación.
|
||||
|
||||
## Dominio personalizado
|
||||
|
||||
1. Ve a los ajustes de tu proyecto
|
||||
2. Ve a "Domains"
|
||||
3. Añade tu dominio personalizado
|
||||
4. Configura el DNS según las instrucciones
|
||||
|
||||
## Limitaciones
|
||||
|
||||
::: warning Archivos grandes
|
||||
Las funciones serverless de Vercel tienen un límite de 50 MB. Como BentoPDF es un sitio estático, esto no debería afectarte, pero los módulos WASM son grandes (~100 MB en total). Asegúrate de que se sirvan desde la carpeta `/public`.
|
||||
:::
|
||||
|
||||
## Solución de problemas
|
||||
|
||||
### Tiempo de espera agotado en la compilación
|
||||
|
||||
Si las compilaciones agotan el tiempo de espera, prueba lo siguiente:
|
||||
|
||||
```json
|
||||
// vercel.json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist"
|
||||
}
|
||||
```
|
||||
|
||||
### Error 404 al recargar
|
||||
|
||||
Añade un `vercel.json` para el enrutamiento SPA:
|
||||
|
||||
```json
|
||||
{
|
||||
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
|
||||
}
|
||||
```
|
||||
|
||||
### La conversión de Word/ODT/Excel a PDF no funciona
|
||||
|
||||
Las conversiones de LibreOffice WASM requieren `SharedArrayBuffer`, que necesita estas cabeceras de respuesta en todas las páginas:
|
||||
|
||||
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||
- `Cross-Origin-Opener-Policy: same-origin`
|
||||
|
||||
Los archivos precomprimidos `.wasm.gz` y `.data.gz` también necesitan `Content-Encoding: gzip` para que el navegador los descomprima.
|
||||
|
||||
Añade esto a tu `vercel.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
|
||||
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/libreoffice-wasm/soffice.wasm.gz",
|
||||
"headers": [
|
||||
{ "key": "Content-Type", "value": "application/wasm" },
|
||||
{ "key": "Content-Encoding", "value": "gzip" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/libreoffice-wasm/soffice.data.gz",
|
||||
"headers": [
|
||||
{ "key": "Content-Type", "value": "application/octet-stream" },
|
||||
{ "key": "Content-Encoding", "value": "gzip" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Para verificarlo, abre la consola de DevTools y ejecuta `console.log(window.crossOriginIsolated)`: debería devolver `true`.
|
||||
Reference in new issue
Block a user