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

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

No files matched your search

+275
View File
@@ -0,0 +1,275 @@
# 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
```
+200
View File
@@ -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"
}
}
```
+154
View File
@@ -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.
+180
View File
@@ -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
+622
View File
@@ -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
```
+266
View File
@@ -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.
+538
View File
@@ -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.
:::
+211
View File
@@ -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.
+139
View File
@@ -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
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](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.
+232
View File
@@ -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
```
+122
View File
@@ -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
[![Deploy with Vercel](https://vercel.com/button)](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`.