dist/ folder that can be served from any platform capable of hosting static files. You can either build from source or download a pre-built dist-{version}.zip from GitHub Releases.
All platforms must send these headers for Word, Excel, and PowerPoint conversions to work:Without them,
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
SharedArrayBuffer is unavailable and LibreOffice WASM will fail to initialize. Verify after deploying:// Run in browser DevTools console — should return true
console.log(window.crossOriginIsolated)
Building from source
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
# Output is in dist/
- Vercel
- Netlify
- Cloudflare Pages
- AWS S3 + CloudFront
- Nginx
- Apache
- GitHub Pages
Vercel offers the simplest deployment experience for static sites.Required
Create
Connect repository
Fork the repository
Fork alam00000/bentopdf to your GitHub account.
Import to Vercel
Go to vercel.com/new, select your forked repository, and configure the project:
| Setting | Value |
|---|---|
| Framework Preset | Vite |
| Build Command | npm run build |
| Output Directory | dist |
| Install Command | npm install |
Add environment variables (optional)
Under Environment Variables, add any of the following:
| Variable | Description |
|---|---|
SIMPLE_MODE | Set to true for a minimal tool-only UI |
VITE_BRAND_NAME | Custom brand name |
VITE_BRAND_LOGO | Logo path relative to public/ |
VITE_FOOTER_TEXT | Custom footer text |
VITE_DEFAULT_LANGUAGE | Default UI language (e.g. fr, de) |
Required vercel.json
Create vercel.json in the project root to add the COOP/COEP headers and configure SPA routing:vercel.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" }
]
}
]
}
Netlify supports both drag-and-drop deployment of the pre-built Required
Create
dist folder and continuous deployment from a Git repository.Option A: Drag-and-drop (no build required)
Download the pre-built distribution
Go to BentoPDF Releases and download the latest
dist-{version}.zip.Deploy to Netlify
Log in to Netlify, go to your team overview, click Add new site → Deploy manually, then drag and drop the zip file.
Option B: Connect repository
Fork and connect
Fork the repository, then in Netlify: Add new site → Import an existing project → GitHub. Select your BentoPDF fork.
Required netlify.toml
Create netlify.toml in the project root:netlify.toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
# Required for SharedArrayBuffer (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
[[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
[[headers]]
for = "/libreoffice-wasm/soffice.data.gz"
[headers.values]
Content-Type = "application/octet-stream"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
[[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
If Office conversions hang at ~55%, open DevTools and check
console.log(window.crossOriginIsolated). If it returns false, the COOP/COEP headers are not being applied — confirm netlify.toml is at the project root and redeploy.Cloudflare Pages offers free unlimited bandwidth with a global CDN.
Required
Create Create
Connect repository
Create a Pages project
Go to Cloudflare Pages, click Create a project, and connect your GitHub repository.
Configure build settings
| Setting | Value |
|---|---|
| Framework preset | None |
| Build command | npm run build |
| Build output directory | dist |
| Root directory | / |
Required _headers and _redirects
Create public/_headers:public/_headers
# Required for SharedArrayBuffer (LibreOffice WASM)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: cross-origin
/libreoffice-wasm/soffice.wasm.gz
Content-Type: application/wasm
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
/libreoffice-wasm/soffice.data.gz
Content-Type: application/octet-stream
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
/*.wasm
Cache-Control: public, max-age=31536000, immutable
Content-Type: application/wasm
public/_redirects for SPA routing:public/_redirects
/* /index.html 200
Host BentoPDF on AWS for full control over infrastructure.
Architecture
User → CloudFront (CDN + response headers) → S3 (static files)
Create an S3 bucket
aws s3 mb s3://your-bentopdf-bucket --region us-east-1
aws s3 website s3://your-bentopdf-bucket \
--index-document index.html \
--error-document index.html
Build and upload
npm run build
aws s3 sync dist/ s3://your-bentopdf-bucket \
--delete \
--cache-control "max-age=31536000"
# Ensure correct MIME type for WASM files
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.wasm" \
--content-type "application/wasm" \
--metadata-directive REPLACE
Create a CloudFront distribution
In the AWS Console: CloudFront → Create distribution.
- Origin domain: your S3 bucket
- Enable Origin Access Control
- Default root object:
index.html
Create a response headers policy
In CloudFront → Policies → Response headers, create a custom policy with these headers and attach it to your distribution’s default cache behavior:
Or via CLI:
| Header | Value |
|---|---|
Cross-Origin-Embedder-Policy | require-corp |
Cross-Origin-Opener-Policy | same-origin |
Cross-Origin-Resource-Policy | cross-origin |
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}
]
}
}'
Fix pre-compressed WASM metadata
The LibreOffice WASM files ship pre-compressed. Set the correct
Content-Type and Content-Encoding so browsers decompress them: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
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
S3 bucket policy
Allow CloudFront to access the bucket via Origin Access Control:{
"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"
}
}
}
]
}
Serve BentoPDF’s
dist/ folder with Nginx on your own server.Build and copy files
npm run build
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
Create the Nginx config
Create
/etc/nginx/sites-available/bentopdf:/etc/nginx/sites-available/bentopdf
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_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;
# WASM MIME type
types {
application/wasm wasm;
}
# Required for SharedArrayBuffer (LibreOffice WASM)
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;
# 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|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
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;
}
}
Nginx
add_header directives in a location block override server-level directives — they do not merge. Every location block that sets its own add_header must also repeat the COOP/COEP headers.Enable the site and restart
sudo ln -s /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Subdirectory deployment
To host at/pdf/, first build with BASE_URL=/pdf/ npm run build, then add this location block:location /pdf/ {
alias /var/www/bentopdf/;
try_files $uri $uri/ /pdf/index.html;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
Serve BentoPDF with Apache using a
Place this Ensure
.htaccess file or a VirtualHost config..htaccess (shared hosting)
Place this .htaccess in the directory where dist/ is extracted:.htaccess
# SPA routing
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]
# Required headers for SharedArrayBuffer (LibreOffice WASM)
<IfModule mod_headers.c>
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Resource-Policy "cross-origin"
</IfModule>
# WASM MIME type
<IfModule mod_mime.c>
AddType application/wasm .wasm
</IfModule>
# Cache static assets
<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"
</IfModule>
VirtualHost config
<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>
AllowOverride All
Require all granted
</Directory>
# Required for SharedArrayBuffer
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Resource-Policy "cross-origin"
</VirtualHost>
mod_headers, mod_rewrite, and mod_expires are enabled:sudo a2enmod headers rewrite expires
sudo systemctl reload apache2
Host BentoPDF directly from your GitHub fork with no third-party services.Once deployed, your site is at
Fork the repository
Fork alam00000/bentopdf to your GitHub account.
Set the BASE_URL variable
Go to Settings → Secrets and Variables → Actions → Variables and add:
| Name | Value |
|---|---|
BASE_URL | /bentopdf (or your repo name if different) |
https://<your-github-username>.github.io/bentopdf.GitHub Pages does not support custom response headers, so
SharedArrayBuffer is unavailable and Office file conversions will not work. All other tools function normally.