fix(nginx): tighten routing + add SEO redirects

fix(seo): canonical -> English for localized pages
docs: reframe Self-Hosted vs Commercial builds + Common Issues
This commit is contained in:
alam00000
2026-05-15 12:13:37 +05:30
parent f24bf4ea98
commit 58a0053778
18 changed files with 405 additions and 266 deletions
+4 -4
View File
@@ -128,25 +128,25 @@
<p class="text-gray-500 mb-4">Or try these popular tools:</p> <p class="text-gray-500 mb-4">Or try these popular tools:</p>
<div class="flex flex-wrap justify-center gap-3"> <div class="flex flex-wrap justify-center gap-3">
<a <a
href="/src/pages/merge-pdf.html" href="/merge-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm" class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
> >
Merge PDF Merge PDF
</a> </a>
<a <a
href="/src/pages/split-pdf.html" href="/split-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm" class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
> >
Split PDF Split PDF
</a> </a>
<a <a
href="/src/pages/compress-pdf.html" href="/compress-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm" class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
> >
Compress PDF Compress PDF
</a> </a>
<a <a
href="/src/pages/edit-pdf.html" href="/edit-pdf"
class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm" class="px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white transition-colors text-sm"
> >
Edit PDF Edit PDF
+79 -26
View File
@@ -41,7 +41,8 @@
- [Self-Hosting Locally](#-self-hosting-locally) - [Self-Hosting Locally](#-self-hosting-locally)
- [Docker Compose / Podman Compose](#-run-with-docker-compose--podman-compose-recommended) - [Docker Compose / Podman Compose](#-run-with-docker-compose--podman-compose-recommended)
- [Podman Quadlet](#-podman-quadlet-systemd-integration) - [Podman Quadlet](#-podman-quadlet-systemd-integration)
- [Simple Mode](#-simple-mode-for-internal-use) - [Self-Hosted Build (Simple Mode)](#-self-hosted-build-simple-mode)
- [Commercial Build](#-commercial-build)
- [Custom Branding](#-custom-branding) - [Custom Branding](#-custom-branding)
- [Disabling Specific Tools](#-disabling-specific-tools) - [Disabling Specific Tools](#-disabling-specific-tools)
- [WASM Configuration](#wasm-configuration) - [WASM Configuration](#wasm-configuration)
@@ -324,30 +325,38 @@ You can run BentoPDF locally for development or personal use.
Run BentoPDF instantly from GitHub Container Registry (Recommended): Run BentoPDF instantly from GitHub Container Registry (Recommended):
```bash ```bash
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
``` ```
Open your browser at: http://localhost:3000 Open your browser at: http://localhost:3000
> [!TIP]
> BentoPDF ships in two builds:
>
> - **Self-Hosted build** — `ghcr.io/alam00000/bentopdf-simple:latest`. Every PDF tool the public site has, **without** the BentoPDF marketing (no hero, FAQ, testimonials, footer). Use this for internal/team/organization deployments. It is **not** a feature-reduced lite version.
> - **Commercial build** — `ghcr.io/alam00000/bentopdf:latest`. The full marketing site, used by bentopdf.com itself and by commercial license holders running public-facing deployments. Includes hero, FAQ, testimonials, and footer.
>
> If in doubt: pull the Self Hosted build.
<details> <details>
<summary><b>Alternative: Using Docker Hub or Podman</b></summary> <summary><b>Alternative: Using Docker Hub or Podman</b></summary>
**Docker Hub:** **Docker Hub:**
```bash ```bash
docker run -p 3000:8080 bentopdfteam/bentopdf:latest docker run -p 3000:8080 bentopdfteam/bentopdf-simple:latest
``` ```
**Podman (GHCR):** **Podman (GHCR):**
```bash ```bash
podman run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest podman run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
``` ```
**Podman (Docker Hub):** **Podman (Docker Hub):**
```bash ```bash
podman run -p 3000:8080 docker.io/bentopdfteam/bentopdf:latest podman run -p 3000:8080 docker.io/bentopdfteam/bentopdf-simple:latest
``` ```
> [!NOTE] > [!NOTE]
@@ -745,8 +754,9 @@ For a more robust setup with auto-restart capabilities:
```yaml ```yaml
services: services:
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest # Recommended image: ghcr.io/alam00000/bentopdf-simple:latest # Self-Hosted build (recommended)
# image: bentopdfteam/bentopdf:latest # Alternative: Docker Hub # image: bentopdfteam/bentopdf-simple:latest # Self-Hosted build (Docker Hub)
# image: ghcr.io/alam00000/bentopdf:latest # Commercial build (bentopdf.com / commercial license holders)
container_name: bentopdf container_name: bentopdf
ports: ports:
- '3000:8080' - '3000:8080'
@@ -777,7 +787,7 @@ Description=BentoPDF - Privacy-first PDF toolkit
After=network-online.target After=network-online.target
[Container] [Container]
Image=ghcr.io/alam00000/bentopdf:latest Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf ContainerName=bentopdf
PublishPort=3000:8080 PublishPort=3000:8080
AutoUpdate=registry AutoUpdate=registry
@@ -798,18 +808,61 @@ systemctl --user enable --now bentopdf
For detailed Quadlet configuration, see [Self-Hosting Docker Guide](https://bentopdf.com/docs/self-hosting/docker). For detailed Quadlet configuration, see [Self-Hosting Docker Guide](https://bentopdf.com/docs/self-hosting/docker).
### 🏢 Simple Mode for Internal Use ### 🏢 Self-Hosted build (Simple Mode)
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content. The Self-Hosted build (the `bentopdf-simple` image, also called Simple Mode) is **functionally identical** to the Commercial build. Every PDF tool is present and behaves the same. It just hides the marketing that only makes sense on bentopdf.com itself or on a commercial public-facing deployment. **It is not a feature reduced or "lite" version.**
**What Simple Mode does:** **What the Self-Hosted build hides** (cosmetic only, no PDF features are removed):
- Hides navigation, hero section, features, FAQ, testimonials, and footer - Navigation bar, hero section, features section, FAQ, testimonials, footer
- Shows only the essential PDF tools
- Updates page title to "PDF Tools" - Updates page title to "PDF Tools"
- Perfect for internal company tools and educational institutions
For more details, see [SIMPLE_MODE.md](SIMPLE_MODE.md). **What the Self-Hosted build keeps** (everything that actually does PDF work):
- All PDF tools (merge, split, edit, sign, OCR, Office conversion, every other tool)
- Custom branding support, all build-time and runtime config
The Commercial build (`ghcr.io/alam00000/bentopdf:latest`) is what powers bentopdf.com itself and is used by commercial license holders running public facing deployments. It adds the hero, FAQ, testimonials, and footer that wouldn't make sense on an internal tool.
If you're self-hosting BentoPDF for your team, organization, or as an internal tool, pull `ghcr.io/alam00000/bentopdf-simple:latest`. For more details, see [SIMPLE_MODE.md](SIMPLE_MODE.md).
### 🏬 Commercial Build
The Commercial build (the `bentopdf` image. Note: no `-simple` suffix) is what powers bentopdf.com itself. It includes the full marketing site (hero, features, FAQ, testimonials, footer) on top of every PDF tool. Use this build when you're running BentoPDF as a **public-facing PDF service under your own brand**. For example:
- You're deploying BentoPDF as a hosted SaaS for end-users (with your own domain and branding)
- You want the landing-page experience (marketing sections + tools), not just the tool surface
- You're a commercial license holder embedding BentoPDF into a commercial product or workflow
**Run it as-is** (carries BentoPDF branding by default. Useful to evaluate what the build looks like):
```bash
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
```
**Build with your own brand** (the typical commercial path — replace the BentoPDF logo, name, and footer):
```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
```
Or set the same variables when building from source — see [Custom Branding](#-custom-branding) below for the full list of options.
**Combine with other build-time flags** (`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, `VITE_USE_CDN`, WASM URL overrides for air-gapped use, etc.) — every option that works on the Self-Hosted build also works here.
> [!IMPORTANT]
> **Licensing**: Running the Commercial build is allowed under both license options BentoPDF ships under:
>
> - **AGPL-3.0** (free): allowed if your deployment publishes its full source code under AGPL — this includes any branding modifications, custom configuration, and any code you build on top of it.
> - **Commercial license** ($79 lifetime): required for closed-source / proprietary deployments — e.g., a private SaaS where you don't open-source your branding fork or surrounding business logic.
>
> See the [Licensing page](https://bentopdf.com/licensing.html) for the full comparison.
### 🎨 Custom Branding ### 🎨 Custom Branding
@@ -870,7 +923,7 @@ docker build --build-arg DISABLE_TOOLS="edit-pdf,sign-pdf,encrypt-pdf" -t bentop
```bash ```bash
docker run -d -p 3000:8080 \ docker run -d -p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \ -v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf:latest ghcr.io/alam00000/bentopdf-simple:latest
``` ```
Both methods can be combined — the lists are merged. For the full list of tool IDs, see the [self-hosting docs](https://bentopdf.com/docs/self-hosting/docker#disabling-specific-tools). Both methods can be combined — the lists are merged. For the full list of tool IDs, see the [self-hosting docs](https://bentopdf.com/docs/self-hosting/docker#disabling-specific-tools).
@@ -905,7 +958,7 @@ docker run -p 8080:8080 bentopdf
By default, BentoPDF listens on port `8080` inside the container. To change this, set the `PORT` environment variable: By default, BentoPDF listens on port `8080` inside the container. To change this, set the `PORT` environment variable:
```bash ```bash
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf:latest docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
``` ```
| Variable | Description | Default | | Variable | Description | Default |
@@ -1053,19 +1106,19 @@ DOCKER_BUILDKIT=1 docker build \
### 📦 Version Management ### 📦 Version Management
BentoPDF supports semantic versioning with multiple container tags available: BentoPDF publishes two image variants. Both ship the same PDF tools. The difference is purely whether the bentopdf.com marketing is included.
**GitHub Container Registry (Recommended):** **Self-Hosted build** — recommended for internal/team/organization deployments:
- **Latest**: `ghcr.io/alam00000/bentopdf-simple:latest`
- **Specific Version**: `ghcr.io/alam00000/bentopdf-simple:1.0.0`
- **Docker Hub**: `bentopdfteam/bentopdf-simple:latest`
**Commercial build** — used by bentopdf.com itself and by commercial license holders running public-facing deployments:
- **Latest**: `ghcr.io/alam00000/bentopdf:latest` - **Latest**: `ghcr.io/alam00000/bentopdf:latest`
- **Specific Version**: `ghcr.io/alam00000/bentopdf:1.0.0` - **Specific Version**: `ghcr.io/alam00000/bentopdf:1.0.0`
- **Version with Prefix**: `ghcr.io/alam00000/bentopdf:v1.0.0` - **Docker Hub**: `bentopdfteam/bentopdf:latest`
**Docker Hub:**
- **Latest**: `bentopdfteam/bentopdf:latest`
- **Specific Version**: `bentopdfteam/bentopdf:1.0.0`
- **Version with Prefix**: `bentopdfteam/bentopdf:v1.0.0`
#### Quick Release #### Quick Release
+38 -32
View File
@@ -1,21 +1,25 @@
# Simple Mode for BentoPDF # Simple Mode for BentoPDF (the Self-Hosted build)
Simple Mode is designed for internal organizational use where you want to hide all branding and marketing content, showing only the essential PDF tools for your users. Simple Mode is what powers the **Self-Hosted build** of BentoPDF (`bentopdf-simple`). It is **functionally identical** to the Commercial build that powers bentopdf.com — every PDF tool is present and behaves the same. It just hides the marketing chrome (hero, FAQ, testimonials, footer) that only makes sense on the public bentopdf.com site or on a commercial public-facing deployment.
## What Simple Mode Does > **Simple Mode is not a feature-reduced "lite" version.** Every PDF tool — merge, split, edit, sign, OCR, Office conversion, every other tool — works identically in both builds. The only difference is the marketing UI around the tools.
When enabled, Simple Mode will: The Commercial build (`ghcr.io/alam00000/bentopdf:latest`) is used by bentopdf.com itself and by commercial license holders running public-facing PDF deployments where the full marketing site makes sense.
- Hide the navigation bar ## What Simple Mode Hides
- Hide the hero section with marketing content
- Hide the features section When enabled, Simple Mode hides the following bentopdf.com-specific marketing UI:
- Hide the security/compliance section
- Hide the FAQ section - Navigation bar
- Hide the testimonials section - Hero section with marketing content
- Hide the support section - Features section
- Hide the footer - Security/compliance section
- Update the page title to "PDF Tools" - FAQ section
- Make the tools section more prominent - Testimonials section
- Support section
- Footer
It also updates the page title to "PDF Tools" and makes the tools section more prominent. **No PDF tools are removed or disabled.**
## How to Enable Simple Mode ## How to Enable Simple Mode
@@ -136,20 +140,20 @@ npx serve dist -p 3000
Open `http://localhost:3000` in your browser. Open `http://localhost:3000` in your browser.
### Method 4: Compare Both Modes ### Method 4: Compare Both Builds Side-by-Side
```bash ```bash
# Test Normal Mode (Docker) # Commercial build (the bentopdf.com look)
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
# Test Simple Mode (Docker) # Self-Hosted build (Simple Mode)
docker run -p 3001:8080 ghcr.io/alam00000/bentopdf-simple:latest docker run -p 3001:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman users: replace 'docker' with 'podman' # Podman users: replace 'docker' with 'podman'
``` ```
- Normal Mode: `http://localhost:3000` - Commercial build: `http://localhost:3000`
- Simple Mode: `http://localhost:3001` - Self-Hosted build: `http://localhost:3001`
## 🔍 What to Look For ## 🔍 What to Look For
@@ -165,19 +169,7 @@ When Simple Mode is working correctly, you should see:
## 📦 Available Container Images ## 📦 Available Container Images
### Normal Mode (Full Branding) ### Self-Hosted build (Simple Mode) — recommended for self-hosting
**GitHub Container Registry (Recommended):**
- `ghcr.io/alam00000/bentopdf:latest`
- `ghcr.io/alam00000/bentopdf:v1.0.0` (versioned)
**Docker Hub:**
- `bentopdfteam/bentopdf:latest`
- `bentopdfteam/bentopdf:v1.0.0` (versioned)
### Simple Mode (Clean Interface)
**GitHub Container Registry (Recommended):** **GitHub Container Registry (Recommended):**
@@ -189,6 +181,20 @@ When Simple Mode is working correctly, you should see:
- `bentopdfteam/bentopdf-simple:latest` - `bentopdfteam/bentopdf-simple:latest`
- `bentopdfteam/bentopdf-simple:v1.0.0` (versioned) - `bentopdfteam/bentopdf-simple:v1.0.0` (versioned)
### Commercial build — used by bentopdf.com and commercial license holders
The full marketing site, including hero/FAQ/testimonials/footer. Pull this only if you specifically want the bentopdf.com look — for example, you're running a public-facing PDF deployment under a commercial license.
**GitHub Container Registry (Recommended):**
- `ghcr.io/alam00000/bentopdf:latest`
- `ghcr.io/alam00000/bentopdf:v1.0.0` (versioned)
**Docker Hub:**
- `bentopdfteam/bentopdf:latest`
- `bentopdfteam/bentopdf:v1.0.0` (versioned)
## 🚀 Production Deployment Examples ## 🚀 Production Deployment Examples
### Docker Compose / Podman Compose ### Docker Compose / Podman Compose
+4 -4
View File
@@ -1,11 +1,11 @@
services: services:
bentopdf: bentopdf:
# GitHub Container Registry (Recommended) # GitHub Container Registry (Recommended)
# simple mode - ghcr.io/alam00000/bentopdf-simple:latest # Self-Hosted build - ghcr.io/alam00000/bentopdf-simple:latest
# default mode - ghcr.io/alam00000/bentopdf:latest # Commercial build - ghcr.io/alam00000/bentopdf:latest
# Docker Hub (Alternative) # Docker Hub (Alternative)
# simple mode - bentopdfteam/bentopdf-simple:latest # Self-Hosted build - bentopdfteam/bentopdf-simple:latest
# default mode - bentopdfteam/bentopdf:latest # Commercial build - bentopdfteam/bentopdf:latest
image: ghcr.io/alam00000/bentopdf-simple:latest image: ghcr.io/alam00000/bentopdf-simple:latest
container_name: bentopdf container_name: bentopdf
restart: unless-stopped restart: unless-stopped
+11
View File
@@ -1,9 +1,20 @@
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
const SITE_URL = 'https://www.bentopdf.com'
export default defineConfig({ export default defineConfig({
title: "BentoPDF Docs", title: "BentoPDF Docs",
description: "Documentation for BentoPDF - The free, open-source, privacy-first PDF toolkit", description: "Documentation for BentoPDF - The free, open-source, privacy-first PDF toolkit",
base: '/docs/', base: '/docs/',
cleanUrls: true,
transformPageData(pageData) {
const relPath = pageData.relativePath.replace(/\.md$/, '')
const slug = relPath === 'index' ? '' : relPath.replace(/\/index$/, '/')
const canonicalUrl = slug ? `${SITE_URL}/docs/${slug}` : `${SITE_URL}/docs/`
pageData.frontmatter.head ??= []
pageData.frontmatter.head.push(['link', { rel: 'canonical', href: canonicalUrl }])
},
themeConfig: { themeConfig: {
logo: '/images/favicon-no-bg.svg', logo: '/images/favicon-no-bg.svg',
+5 -2
View File
@@ -24,14 +24,17 @@ Visit [bentopdf.com](https://bentopdf.com) to use BentoPDF instantly—no instal
> `http://localhost` works for local testing because browsers treat loopback as trustworthy. `http://192.168.x.x` or other LAN IPs usually do not, so Word/Excel/PowerPoint conversions will require HTTPS when accessed from other devices on your network. > `http://localhost` works for local testing because browsers treat loopback as trustworthy. `http://192.168.x.x` or other LAN IPs usually do not, so Word/Excel/PowerPoint conversions will require HTTPS when accessed from other devices on your network.
```bash ```bash
# Pull and run the Docker image # Pull and run the Docker image (self-hosted build)
docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf:latest docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Or use Docker Compose # Or use Docker Compose
curl -O https://raw.githubusercontent.com/alam00000/bentopdf/main/docker-compose.yml curl -O https://raw.githubusercontent.com/alam00000/bentopdf/main/docker-compose.yml
docker compose up -d docker compose up -d
``` ```
> [!TIP]
> BentoPDF ships in two builds: the **Self-Hosted build** (`bentopdf-simple`, used above) for internal/team/organization deployments, and the **Commercial build** (`bentopdf:latest`) used by bentopdf.com itself and by commercial license holders running public-facing deployments. Both ship the same PDF tools — the Commercial build adds the bentopdf.com hero, FAQ, testimonials, and footer. If in doubt, use the Self-Hosted build.
Then open `http://localhost:3000` in your browser. Then open `http://localhost:3000` in your browser.
> [!NOTE] > [!NOTE]
+37 -53
View File
@@ -19,19 +19,27 @@ The easiest way to self-host BentoPDF in a production environment.
## Quick Start ## Quick Start
> [!TIP]
> BentoPDF ships in two builds:
>
> - **Self-Hosted build** — `ghcr.io/alam00000/bentopdf-simple:latest`. Every PDF tool, **without** the marketing chrome (no hero, FAQ, testimonials, footer). Use this for internal/team/organization deployments. It is **not** a feature-reduced "lite" version.
> - **Commercial build** — `ghcr.io/alam00000/bentopdf:latest`. The full marketing site, used by bentopdf.com itself and by commercial license holders running public-facing deployments.
>
> If in doubt: pull the Self-Hosted build.
```bash ```bash
# Docker # Docker
docker run -d \ docker run -d \
--name bentopdf \ --name bentopdf \
-p 3000:8080 \ -p 3000:8080 \
--restart unless-stopped \ --restart unless-stopped \
ghcr.io/alam00000/bentopdf:latest ghcr.io/alam00000/bentopdf-simple:latest
# Podman # Podman
podman run -d \ podman run -d \
--name bentopdf \ --name bentopdf \
-p 3000:8080 \ -p 3000:8080 \
ghcr.io/alam00000/bentopdf:latest ghcr.io/alam00000/bentopdf-simple:latest
``` ```
## Docker Compose / Podman Compose ## Docker Compose / Podman Compose
@@ -41,7 +49,7 @@ Create `docker-compose.yml`:
```yaml ```yaml
services: services:
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf-simple:latest
container_name: bentopdf container_name: bentopdf
ports: ports:
- '3000:8080' - '3000:8080'
@@ -90,23 +98,23 @@ docker run -d -p 3000:8080 bentopdf:custom
## Environment Variables ## Environment Variables
| Variable | Description | Default | | Variable | Description | Default |
| ------------------------------------ | ------------------------------------------- | -------------------------------------------------------------- | | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `SIMPLE_MODE` | Build without LibreOffice tools | `false` | | `SIMPLE_MODE` | Build the Self-Hosted variant (hides bentopdf.com marketing chrome — hero, FAQ, testimonials, footer). All PDF tools remain. | `false` |
| `BASE_URL` | Deploy to subdirectory | `/` | | `BASE_URL` | Deploy to subdirectory | `/` |
| `VITE_WASM_PYMUPDF_URL` | PyMuPDF WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` | | `VITE_WASM_PYMUPDF_URL` | PyMuPDF WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
| `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` | | `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` |
| `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` | | `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` |
| `VITE_TESSERACT_WORKER_URL` | OCR worker script URL | _(empty; use Tesseract.js default CDN)_ | | `VITE_TESSERACT_WORKER_URL` | OCR worker script URL | _(empty; use Tesseract.js default CDN)_ |
| `VITE_TESSERACT_CORE_URL` | OCR core runtime directory | _(empty; use Tesseract.js default CDN)_ | | `VITE_TESSERACT_CORE_URL` | OCR core runtime directory | _(empty; use Tesseract.js default CDN)_ |
| `VITE_TESSERACT_LANG_URL` | OCR traineddata directory | _(empty; use Tesseract.js default CDN)_ | | `VITE_TESSERACT_LANG_URL` | OCR traineddata directory | _(empty; use Tesseract.js default CDN)_ |
| `VITE_TESSERACT_AVAILABLE_LANGUAGES` | Comma-separated OCR languages exposed in UI | _(empty; show full catalog)_ | | `VITE_TESSERACT_AVAILABLE_LANGUAGES` | Comma-separated OCR languages exposed in UI | _(empty; show full catalog)_ |
| `VITE_OCR_FONT_BASE_URL` | OCR text-layer font directory | _(empty; use remote Noto font URLs)_ | | `VITE_OCR_FONT_BASE_URL` | OCR text-layer font directory | _(empty; use remote Noto font URLs)_ |
| `VITE_DEFAULT_LANGUAGE` | Default UI language | `en` | | `VITE_DEFAULT_LANGUAGE` | Default UI language | `en` |
| `VITE_BRAND_NAME` | Custom brand name | `BentoPDF` | | `VITE_BRAND_NAME` | Custom brand name | `BentoPDF` |
| `VITE_BRAND_LOGO` | Logo path relative to `public/` | `images/favicon-no-bg.svg` | | `VITE_BRAND_LOGO` | Logo path relative to `public/` | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | Custom footer/copyright text | `© 2026 BentoPDF. All rights reserved.` | | `VITE_FOOTER_TEXT` | Custom footer/copyright text | `© 2026 BentoPDF. All rights reserved.` |
| `DISABLE_TOOLS` | Comma-separated tool IDs to hide | _(empty; all tools enabled)_ | | `DISABLE_TOOLS` | Comma-separated tool IDs to hide | _(empty; all tools enabled)_ |
WASM module URLs are pre-configured with CDN defaults — all advanced features work out of the box. Override these for air-gapped or self-hosted deployments. WASM module URLs are pre-configured with CDN defaults — all advanced features work out of the box. Override these for air-gapped or self-hosted deployments.
@@ -199,7 +207,7 @@ Mount a `config.json` file into the served directory — no rebuild needed:
docker run -d \ docker run -d \
-p 3000:8080 \ -p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \ -v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf:latest ghcr.io/alam00000/bentopdf-simple:latest
``` ```
Or with Docker Compose: Or with Docker Compose:
@@ -207,7 +215,7 @@ Or with Docker Compose:
```yaml ```yaml
services: services:
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf-simple:latest
ports: ports:
- '3000:8080' - '3000:8080'
volumes: volumes:
@@ -324,7 +332,7 @@ Set a variable to empty string to disable that module (users must configure manu
By default, BentoPDF listens on port `8080` inside the container. To change this, set the `PORT` environment variable at runtime: By default, BentoPDF listens on port `8080` inside the container. To change this, set the `PORT` environment variable at runtime:
```bash ```bash
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf:latest docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
``` ```
| Variable | Description | Default | | Variable | Description | Default |
@@ -410,7 +418,7 @@ services:
- ./letsencrypt:/letsencrypt - ./letsencrypt:/letsencrypt
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf-simple:latest
labels: labels:
- 'traefik.enable=true' - 'traefik.enable=true'
- 'traefik.http.routers.bentopdf.rule=Host(`pdf.example.com`)' - 'traefik.http.routers.bentopdf.rule=Host(`pdf.example.com`)'
@@ -438,7 +446,7 @@ services:
- caddy_data:/data - caddy_data:/data
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf-simple:latest
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -460,7 +468,7 @@ pdf.example.com {
```yaml ```yaml
services: services:
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf-simple:latest
deploy: deploy:
resources: resources:
limits: limits:
@@ -486,7 +494,7 @@ After=network-online.target
Wants=network-online.target Wants=network-online.target
[Container] [Container]
Image=ghcr.io/alam00000/bentopdf:latest Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf ContainerName=bentopdf
PublishPort=3000:8080 PublishPort=3000:8080
AutoUpdate=registry AutoUpdate=registry
@@ -521,30 +529,6 @@ journalctl --user -u bentopdf -f
> [!TIP] > [!TIP]
> For system-wide deployment, use `systemctl` without `--user` flag and place the file in `/etc/containers/systemd/`. > For system-wide deployment, use `systemctl` without `--user` flag and place the file in `/etc/containers/systemd/`.
### Simple Mode Quadlet
For Simple Mode deployment, create `bentopdf-simple.container`:
```ini
[Unit]
Description=BentoPDF Simple Mode - Clean PDF toolkit
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf-simple
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
```
### Quadlet with Health Check ### Quadlet with Health Check
```ini ```ini
@@ -554,7 +538,7 @@ After=network-online.target
Wants=network-online.target Wants=network-online.target
[Container] [Container]
Image=ghcr.io/alam00000/bentopdf:latest Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf ContainerName=bentopdf
PublishPort=3000:8080 PublishPort=3000:8080
AutoUpdate=registry AutoUpdate=registry
@@ -600,7 +584,7 @@ Then reference it in your container file:
```ini ```ini
[Container] [Container]
Image=ghcr.io/alam00000/bentopdf:latest Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf ContainerName=bentopdf
PublishPort=3000:8080 PublishPort=3000:8080
Network=bentopdf.network Network=bentopdf.network
+68 -17
View File
@@ -6,6 +6,14 @@ BentoPDF can be self-hosted on your own infrastructure. This guide covers variou
The fastest way to self-host BentoPDF: The fastest way to self-host BentoPDF:
> [!TIP]
> BentoPDF ships in two builds:
>
> - **Self-Hosted build** — `ghcr.io/alam00000/bentopdf-simple:latest`. Every PDF tool, **without** the marketing chrome (no hero, FAQ, testimonials, footer). Use this for internal/team/organization deployments. It is **not** a feature-reduced "lite" version.
> - **Commercial build** — `ghcr.io/alam00000/bentopdf:latest`. The full marketing site, used by bentopdf.com itself and by commercial license holders running public-facing deployments.
>
> If in doubt: pull the Self-Hosted build.
> [!IMPORTANT] > [!IMPORTANT]
> Office file conversion requires `SharedArrayBuffer`, which means the app must be both cross-origin isolated and served from a secure context. The official image already sends the required COOP/COEP headers, but browsers still disable `SharedArrayBuffer` on plain HTTP local-network origins such as `http://192.168.x.x`. > Office file conversion requires `SharedArrayBuffer`, which means the app must be both cross-origin isolated and served from a secure context. The official image already sends the required COOP/COEP headers, but browsers still disable `SharedArrayBuffer` on plain HTTP local-network origins such as `http://192.168.x.x`.
> >
@@ -13,10 +21,10 @@ The fastest way to self-host BentoPDF:
```bash ```bash
# Docker # Docker
docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf:latest docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman # Podman
podman run -d -p 3000:8080 ghcr.io/alam00000/bentopdf:latest podman run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
``` ```
Or with Docker Compose / Podman Compose: Or with Docker Compose / Podman Compose:
@@ -25,7 +33,7 @@ Or with Docker Compose / Podman Compose:
# docker-compose.yml # docker-compose.yml
services: services:
bentopdf: bentopdf:
image: ghcr.io/alam00000/bentopdf:latest image: ghcr.io/alam00000/bentopdf-simple:latest
ports: ports:
- '3000:8080' - '3000:8080'
restart: unless-stopped restart: unless-stopped
@@ -45,7 +53,7 @@ Run BentoPDF as a systemd service. Create `~/.config/containers/systemd/bentopdf
```ini ```ini
[Container] [Container]
Image=ghcr.io/alam00000/bentopdf:latest Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf ContainerName=bentopdf
PublishPort=3000:8080 PublishPort=3000:8080
AutoUpdate=registry AutoUpdate=registry
@@ -78,28 +86,71 @@ npm run build
## Configuration Options ## Configuration Options
### Simple Mode ### Self-Hosted build (Simple Mode)
Simple Mode is designed for internal organizational use where you want to hide all branding and marketing content, showing only the essential PDF tools. The Self-Hosted build (the `bentopdf-simple` image, also called Simple Mode) is **functionally identical** to the Commercial build — every PDF tool is present and behaves the same. It just hides the marketing chrome that only makes sense on bentopdf.com itself or on a commercial public-facing deployment. **It is not a feature-reduced or "lite" version.**
**What Simple Mode hides:** **What the Self-Hosted build hides** (cosmetic only — no PDF features are removed):
- Navigation bar - Navigation bar, hero section, features section, FAQ, testimonials, footer
- Hero section with marketing content
- Features, FAQ, testimonials sections
- Footer
- Updates page title to "PDF Tools" - Updates page title to "PDF Tools"
```bash **What the Self-Hosted build keeps** (everything that actually does PDF work):
# Build with Simple Mode
SIMPLE_MODE=true npm run build
# Or use the pre-built Docker image - Every PDF tool (merge, split, edit, sign, OCR, Office conversion, etc.)
docker run -p 3000:8080 bentopdfteam/bentopdf-simple:latest - Custom branding support, all build-time and runtime config
The Commercial build (`ghcr.io/alam00000/bentopdf:latest`) is what powers bentopdf.com itself and is used by commercial license holders running public-facing deployments — it adds the hero, FAQ, testimonials, and footer that wouldn't make sense on an internal tool.
If you're self-hosting BentoPDF for your team, organization, or as an internal tool, pull the Self-Hosted build:
```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
``` ```
See [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md) for full details. See [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md) for full details.
### Commercial build (public-facing deployments with your own brand)
The Commercial build (the `bentopdf` image — no `-simple` suffix) is what powers bentopdf.com itself. It includes the full marketing site (hero, features, FAQ, testimonials, footer) on top of every PDF tool. Use this build when you want to **deploy BentoPDF as a public-facing PDF service under your own brand** — for example:
- You're running BentoPDF as a hosted SaaS for end-users on your own domain
- You want the landing-page experience (marketing sections + tools), not just the bare tool surface
- You're a commercial license holder embedding BentoPDF into a commercial product
**Run it as-is** (BentoPDF branding — useful to preview the build):
```bash
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
```
**Build with your own brand** (the typical commercial workflow):
```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
```
Every other build-time option (`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, WASM URL overrides, etc.) works the same way it does on the Self-Hosted build.
::: warning Licensing
Running the Commercial build is allowed under either of BentoPDF's two license options:
- **AGPL-3.0** (free) — allowed if your deployment publishes its full source code under AGPL, including any branding modifications and surrounding business logic.
- **Commercial license** ($79 lifetime) — required for closed-source / proprietary deployments where you don't open-source your branding fork or business code.
See the [Licensing page](https://bentopdf.com/licensing.html) for the full comparison. AGPL-licensed WASM modules (PyMuPDF, Ghostscript, CoherentPDF) load from a CDN at runtime, so they don't enter your image and don't change your licensing posture.
:::
### Base URL ### Base URL
Deploy to a subdirectory: Deploy to a subdirectory:
@@ -161,7 +212,7 @@ DISABLE_TOOLS="edit-pdf,sign-pdf" npm run build
```bash ```bash
docker run -d -p 3000:8080 \ docker run -d -p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \ -v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf:latest ghcr.io/alam00000/bentopdf-simple:latest
``` ```
Both methods can be combined — the lists are merged. Both methods can be combined — the lists are merged.
+33 -5
View File
@@ -39,6 +39,35 @@ http {
expires 1y; expires 1y;
} }
if ($request_uri ~ ^/ua(/.*)?$) {
return 301 /uk$1;
}
if ($request_uri ~ ^/tools/$) {
return 301 /tools;
}
if ($request_uri ~ ^/tools/([a-z0-9][a-z0-9-]+?)(?:\.html)?(\?.*)?$) {
return 301 /$1$2;
}
if ($request_uri ~ ^/(getting-started|contributing)(\?.*)?$) {
return 301 /docs/$1$2;
}
if ($request_uri ~ ^/self-hosting(/.*)?$) {
return 301 /docs/self-hosting$1;
}
if ($request_uri ~ ^/docs/(.+?)/index\.html(\?.*)?$) {
return 301 /docs/$1/$2;
}
if ($request_uri ~ ^/docs/index\.html(\?.*)?$) {
return 301 /docs/$1;
}
if ($request_uri ~ ^/docs/(.+?)\.html(\?.*)?$) {
return 301 /docs/$1$2;
}
if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW|uk|ja))?/index\.html(\?.*)?$) { if ($request_uri ~ ^(/(?:en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW|uk|ja))?/index\.html(\?.*)?$) {
return 301 $1/$2; return 301 $1/$2;
} }
@@ -46,13 +75,12 @@ http {
return 301 $1/$2$3; return 301 $1/$2$3;
} }
location ~ ^/(en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW|uk|ja)(/.*)?$ { location ~ /src/pages/ {
try_files $uri $uri/ $uri.html /$1/index.html /index.html; return 404;
expires 5m;
} }
location ~ ^/(.+?)/(en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW|uk|ja)(/.*)?$ { location ~ ^/(en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW|uk|ja)(/.*)?$ {
try_files $uri $uri/ $uri.html /$1/$2/index.html /$1/index.html /index.html; try_files $uri $uri/ $uri.html =404;
expires 5m; expires 5m;
} }
+47 -47
View File
@@ -112,247 +112,247 @@
<div <div
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"
> >
<a href="/src/pages/word-to-pdf.html" class="tool-card"> <a href="/word-to-pdf" class="tool-card">
<i <i
class="ph ph-microsoft-word-logo text-4xl text-indigo-400 mb-2" class="ph ph-microsoft-word-logo text-4xl text-indigo-400 mb-2"
></i> ></i>
<h3 class="font-semibold text-white">Word to PDF</h3> <h3 class="font-semibold text-white">Word to PDF</h3>
<p class="text-gray-400 text-sm">DOCX, DOC to PDF</p> <p class="text-gray-400 text-sm">DOCX, DOC to PDF</p>
</a> </a>
<a href="/src/pages/excel-to-pdf.html" class="tool-card"> <a href="/excel-to-pdf" class="tool-card">
<i <i
class="ph ph-microsoft-excel-logo text-4xl text-indigo-400 mb-2" class="ph ph-microsoft-excel-logo text-4xl text-indigo-400 mb-2"
></i> ></i>
<h3 class="font-semibold text-white">Excel to PDF</h3> <h3 class="font-semibold text-white">Excel to PDF</h3>
<p class="text-gray-400 text-sm">XLSX, XLS to PDF</p> <p class="text-gray-400 text-sm">XLSX, XLS to PDF</p>
</a> </a>
<a href="/src/pages/powerpoint-to-pdf.html" class="tool-card"> <a href="/powerpoint-to-pdf" class="tool-card">
<i <i
class="ph ph-microsoft-powerpoint-logo text-4xl text-indigo-400 mb-2" class="ph ph-microsoft-powerpoint-logo text-4xl text-indigo-400 mb-2"
></i> ></i>
<h3 class="font-semibold text-white">PowerPoint to PDF</h3> <h3 class="font-semibold text-white">PowerPoint to PDF</h3>
<p class="text-gray-400 text-sm">PPTX, PPT to PDF</p> <p class="text-gray-400 text-sm">PPTX, PPT to PDF</p>
</a> </a>
<a href="/src/pages/jpg-to-pdf.html" class="tool-card"> <a href="/jpg-to-pdf" class="tool-card">
<i class="ph ph-file-jpg text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-jpg text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">JPG to PDF</h3> <h3 class="font-semibold text-white">JPG to PDF</h3>
<p class="text-gray-400 text-sm">Convert images</p> <p class="text-gray-400 text-sm">Convert images</p>
</a> </a>
<a href="/src/pages/png-to-pdf.html" class="tool-card"> <a href="/png-to-pdf" class="tool-card">
<i class="ph ph-file-png text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-png text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PNG to PDF</h3> <h3 class="font-semibold text-white">PNG to PDF</h3>
<p class="text-gray-400 text-sm">Convert images</p> <p class="text-gray-400 text-sm">Convert images</p>
</a> </a>
<a href="/src/pages/image-to-pdf.html" class="tool-card"> <a href="/image-to-pdf" class="tool-card">
<i class="ph ph-images text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-images text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Image to PDF</h3> <h3 class="font-semibold text-white">Image to PDF</h3>
<p class="text-gray-400 text-sm">Any image to PDF</p> <p class="text-gray-400 text-sm">Any image to PDF</p>
</a> </a>
<a href="/src/pages/webp-to-pdf.html" class="tool-card"> <a href="/webp-to-pdf" class="tool-card">
<i class="ph ph-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">WebP to PDF</h3> <h3 class="font-semibold text-white">WebP to PDF</h3>
<p class="text-gray-400 text-sm">Modern format</p> <p class="text-gray-400 text-sm">Modern format</p>
</a> </a>
<a href="/src/pages/svg-to-pdf.html" class="tool-card"> <a href="/svg-to-pdf" class="tool-card">
<i class="ph ph-file-svg text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-svg text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">SVG to PDF</h3> <h3 class="font-semibold text-white">SVG to PDF</h3>
<p class="text-gray-400 text-sm">Vector to PDF</p> <p class="text-gray-400 text-sm">Vector to PDF</p>
</a> </a>
<a href="/src/pages/heic-to-pdf.html" class="tool-card"> <a href="/heic-to-pdf" class="tool-card">
<i class="ph ph-device-mobile text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-device-mobile text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">HEIC to PDF</h3> <h3 class="font-semibold text-white">HEIC to PDF</h3>
<p class="text-gray-400 text-sm">iPhone photos</p> <p class="text-gray-400 text-sm">iPhone photos</p>
</a> </a>
<a href="/src/pages/tiff-to-pdf.html" class="tool-card"> <a href="/tiff-to-pdf" class="tool-card">
<i class="ph ph-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">TIFF to PDF</h3> <h3 class="font-semibold text-white">TIFF to PDF</h3>
<p class="text-gray-400 text-sm">Convert TIFF</p> <p class="text-gray-400 text-sm">Convert TIFF</p>
</a> </a>
<a href="/src/pages/bmp-to-pdf.html" class="tool-card"> <a href="/bmp-to-pdf" class="tool-card">
<i class="ph ph-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">BMP to PDF</h3> <h3 class="font-semibold text-white">BMP to PDF</h3>
<p class="text-gray-400 text-sm">Convert BMP</p> <p class="text-gray-400 text-sm">Convert BMP</p>
</a> </a>
<a href="/src/pages/txt-to-pdf.html" class="tool-card"> <a href="/txt-to-pdf" class="tool-card">
<i class="ph ph-text-t text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-text-t text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Text to PDF</h3> <h3 class="font-semibold text-white">Text to PDF</h3>
<p class="text-gray-400 text-sm">Plain text</p> <p class="text-gray-400 text-sm">Plain text</p>
</a> </a>
<a href="/src/pages/markdown-to-pdf.html" class="tool-card"> <a href="/markdown-to-pdf" class="tool-card">
<i class="ph ph-markdown-logo text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-markdown-logo text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Markdown to PDF</h3> <h3 class="font-semibold text-white">Markdown to PDF</h3>
<p class="text-gray-400 text-sm">MD to PDF</p> <p class="text-gray-400 text-sm">MD to PDF</p>
</a> </a>
<a href="/src/pages/csv-to-pdf.html" class="tool-card"> <a href="/csv-to-pdf" class="tool-card">
<i class="ph ph-file-csv text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-csv text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">CSV to PDF</h3> <h3 class="font-semibold text-white">CSV to PDF</h3>
<p class="text-gray-400 text-sm">Spreadsheet</p> <p class="text-gray-400 text-sm">Spreadsheet</p>
</a> </a>
<a href="/src/pages/json-to-pdf.html" class="tool-card"> <a href="/json-to-pdf" class="tool-card">
<i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">JSON to PDF</h3> <h3 class="font-semibold text-white">JSON to PDF</h3>
<p class="text-gray-400 text-sm">Data to PDF</p> <p class="text-gray-400 text-sm">Data to PDF</p>
</a> </a>
<a href="/src/pages/pdf-to-docx.html" class="tool-card"> <a href="/pdf-to-docx" class="tool-card">
<i <i
class="ph ph-microsoft-word-logo text-4xl text-indigo-400 mb-2" class="ph ph-microsoft-word-logo text-4xl text-indigo-400 mb-2"
></i> ></i>
<h3 class="font-semibold text-white">PDF to Word</h3> <h3 class="font-semibold text-white">PDF to Word</h3>
<p class="text-gray-400 text-sm">Editable DOCX</p> <p class="text-gray-400 text-sm">Editable DOCX</p>
</a> </a>
<a href="/src/pages/pdf-to-excel.html" class="tool-card"> <a href="/pdf-to-excel" class="tool-card">
<i <i
class="ph ph-microsoft-excel-logo text-4xl text-indigo-400 mb-2" class="ph ph-microsoft-excel-logo text-4xl text-indigo-400 mb-2"
></i> ></i>
<h3 class="font-semibold text-white">PDF to Excel</h3> <h3 class="font-semibold text-white">PDF to Excel</h3>
<p class="text-gray-400 text-sm">Extract tables</p> <p class="text-gray-400 text-sm">Extract tables</p>
</a> </a>
<a href="/src/pages/pdf-to-jpg.html" class="tool-card"> <a href="/pdf-to-jpg" class="tool-card">
<i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to JPG</h3> <h3 class="font-semibold text-white">PDF to JPG</h3>
<p class="text-gray-400 text-sm">Convert to images</p> <p class="text-gray-400 text-sm">Convert to images</p>
</a> </a>
<a href="/src/pages/pdf-to-png.html" class="tool-card"> <a href="/pdf-to-png" class="tool-card">
<i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to PNG</h3> <h3 class="font-semibold text-white">PDF to PNG</h3>
<p class="text-gray-400 text-sm">Convert to images</p> <p class="text-gray-400 text-sm">Convert to images</p>
</a> </a>
<a href="/src/pages/pdf-to-text.html" class="tool-card"> <a href="/pdf-to-text" class="tool-card">
<i class="ph ph-text-aa text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-text-aa text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to Text</h3> <h3 class="font-semibold text-white">PDF to Text</h3>
<p class="text-gray-400 text-sm">Extract text</p> <p class="text-gray-400 text-sm">Extract text</p>
</a> </a>
<a href="/src/pages/pdf-to-markdown.html" class="tool-card"> <a href="/pdf-to-markdown" class="tool-card">
<i class="ph ph-markdown-logo text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-markdown-logo text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to Markdown</h3> <h3 class="font-semibold text-white">PDF to Markdown</h3>
<p class="text-gray-400 text-sm">Extract as MD</p> <p class="text-gray-400 text-sm">Extract as MD</p>
</a> </a>
<a href="/src/pages/pdf-to-csv.html" class="tool-card"> <a href="/pdf-to-csv" class="tool-card">
<i class="ph ph-file-csv text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-csv text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to CSV</h3> <h3 class="font-semibold text-white">PDF to CSV</h3>
<p class="text-gray-400 text-sm">Extract tables</p> <p class="text-gray-400 text-sm">Extract tables</p>
</a> </a>
<a href="/src/pages/pdf-to-json.html" class="tool-card"> <a href="/pdf-to-json" class="tool-card">
<i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to JSON</h3> <h3 class="font-semibold text-white">PDF to JSON</h3>
<p class="text-gray-400 text-sm">Structured data</p> <p class="text-gray-400 text-sm">Structured data</p>
</a> </a>
<a href="/src/pages/pdf-to-svg.html" class="tool-card"> <a href="/pdf-to-svg" class="tool-card">
<i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to SVG</h3> <h3 class="font-semibold text-white">PDF to SVG</h3>
<p class="text-gray-400 text-sm">Vector output</p> <p class="text-gray-400 text-sm">Vector output</p>
</a> </a>
<a href="/src/pages/pdf-to-webp.html" class="tool-card"> <a href="/pdf-to-webp" class="tool-card">
<i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to WebP</h3> <h3 class="font-semibold text-white">PDF to WebP</h3>
<p class="text-gray-400 text-sm">Modern format</p> <p class="text-gray-400 text-sm">Modern format</p>
</a> </a>
<a href="/src/pages/pdf-to-tiff.html" class="tool-card"> <a href="/pdf-to-tiff" class="tool-card">
<i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to TIFF</h3> <h3 class="font-semibold text-white">PDF to TIFF</h3>
<p class="text-gray-400 text-sm">Convert to TIFF</p> <p class="text-gray-400 text-sm">Convert to TIFF</p>
</a> </a>
<a href="/src/pages/pdf-to-bmp.html" class="tool-card"> <a href="/pdf-to-bmp" class="tool-card">
<i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to BMP</h3> <h3 class="font-semibold text-white">PDF to BMP</h3>
<p class="text-gray-400 text-sm">Convert to BMP</p> <p class="text-gray-400 text-sm">Convert to BMP</p>
</a> </a>
<a href="/src/pages/pdf-to-greyscale.html" class="tool-card"> <a href="/pdf-to-greyscale" class="tool-card">
<i class="ph ph-palette text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-palette text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to Greyscale</h3> <h3 class="font-semibold text-white">PDF to Greyscale</h3>
<p class="text-gray-400 text-sm">Remove colors</p> <p class="text-gray-400 text-sm">Remove colors</p>
</a> </a>
<a href="/src/pages/pdf-to-pdfa.html" class="tool-card"> <a href="/pdf-to-pdfa" class="tool-card">
<i class="ph ph-archive text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-archive text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to PDF/A</h3> <h3 class="font-semibold text-white">PDF to PDF/A</h3>
<p class="text-gray-400 text-sm">Archive format</p> <p class="text-gray-400 text-sm">Archive format</p>
</a> </a>
<a href="/src/pages/pdf-to-zip.html" class="tool-card"> <a href="/pdf-to-zip" class="tool-card">
<i class="ph ph-file-zip text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-zip text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF to ZIP</h3> <h3 class="font-semibold text-white">PDF to ZIP</h3>
<p class="text-gray-400 text-sm">Package files</p> <p class="text-gray-400 text-sm">Package files</p>
</a> </a>
<a href="/src/pages/odt-to-pdf.html" class="tool-card"> <a href="/odt-to-pdf" class="tool-card">
<i class="ph ph-file text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">ODT to PDF</h3> <h3 class="font-semibold text-white">ODT to PDF</h3>
<p class="text-gray-400 text-sm">OpenDocument</p> <p class="text-gray-400 text-sm">OpenDocument</p>
</a> </a>
<a href="/src/pages/ods-to-pdf.html" class="tool-card"> <a href="/ods-to-pdf" class="tool-card">
<i class="ph ph-table text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-table text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">ODS to PDF</h3> <h3 class="font-semibold text-white">ODS to PDF</h3>
<p class="text-gray-400 text-sm">OpenDocument</p> <p class="text-gray-400 text-sm">OpenDocument</p>
</a> </a>
<a href="/src/pages/odp-to-pdf.html" class="tool-card"> <a href="/odp-to-pdf" class="tool-card">
<i class="ph ph-presentation text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-presentation text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">ODP to PDF</h3> <h3 class="font-semibold text-white">ODP to PDF</h3>
<p class="text-gray-400 text-sm">OpenDocument</p> <p class="text-gray-400 text-sm">OpenDocument</p>
</a> </a>
<a href="/src/pages/odg-to-pdf.html" class="tool-card"> <a href="/odg-to-pdf" class="tool-card">
<i class="ph ph-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">ODG to PDF</h3> <h3 class="font-semibold text-white">ODG to PDF</h3>
<p class="text-gray-400 text-sm">Graphics</p> <p class="text-gray-400 text-sm">Graphics</p>
</a> </a>
<a href="/src/pages/rtf-to-pdf.html" class="tool-card"> <a href="/rtf-to-pdf" class="tool-card">
<i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">RTF to PDF</h3> <h3 class="font-semibold text-white">RTF to PDF</h3>
<p class="text-gray-400 text-sm">Rich text</p> <p class="text-gray-400 text-sm">Rich text</p>
</a> </a>
<a href="/src/pages/epub-to-pdf.html" class="tool-card"> <a href="/epub-to-pdf" class="tool-card">
<i class="ph ph-book-open-text text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-book-open-text text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">EPUB to PDF</h3> <h3 class="font-semibold text-white">EPUB to PDF</h3>
<p class="text-gray-400 text-sm">E-book format</p> <p class="text-gray-400 text-sm">E-book format</p>
</a> </a>
<a href="/src/pages/mobi-to-pdf.html" class="tool-card"> <a href="/mobi-to-pdf" class="tool-card">
<i class="ph ph-book-open-text text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-book-open-text text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">MOBI to PDF</h3> <h3 class="font-semibold text-white">MOBI to PDF</h3>
<p class="text-gray-400 text-sm">Kindle format</p> <p class="text-gray-400 text-sm">Kindle format</p>
</a> </a>
<a href="/src/pages/cbz-to-pdf.html" class="tool-card"> <a href="/cbz-to-pdf" class="tool-card">
<i class="ph ph-book-open text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-book-open text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">CBZ to PDF</h3> <h3 class="font-semibold text-white">CBZ to PDF</h3>
<p class="text-gray-400 text-sm">Comic archive</p> <p class="text-gray-400 text-sm">Comic archive</p>
</a> </a>
<a href="/src/pages/fb2-to-pdf.html" class="tool-card"> <a href="/fb2-to-pdf" class="tool-card">
<i class="ph ph-book-bookmark text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-book-bookmark text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">FB2 to PDF</h3> <h3 class="font-semibold text-white">FB2 to PDF</h3>
<p class="text-gray-400 text-sm">FictionBook</p> <p class="text-gray-400 text-sm">FictionBook</p>
</a> </a>
<a href="/src/pages/psd-to-pdf.html" class="tool-card"> <a href="/psd-to-pdf" class="tool-card">
<i class="ph ph-image text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-image text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PSD to PDF</h3> <h3 class="font-semibold text-white">PSD to PDF</h3>
<p class="text-gray-400 text-sm">Photoshop</p> <p class="text-gray-400 text-sm">Photoshop</p>
</a> </a>
<a href="/src/pages/xps-to-pdf.html" class="tool-card"> <a href="/xps-to-pdf" class="tool-card">
<i class="ph ph-scan text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-scan text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">XPS to PDF</h3> <h3 class="font-semibold text-white">XPS to PDF</h3>
<p class="text-gray-400 text-sm">Microsoft XPS</p> <p class="text-gray-400 text-sm">Microsoft XPS</p>
</a> </a>
<a href="/src/pages/pages-to-pdf.html" class="tool-card"> <a href="/pages-to-pdf" class="tool-card">
<i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Pages to PDF</h3> <h3 class="font-semibold text-white">Pages to PDF</h3>
<p class="text-gray-400 text-sm">Apple Pages</p> <p class="text-gray-400 text-sm">Apple Pages</p>
</a> </a>
<a href="/src/pages/pub-to-pdf.html" class="tool-card"> <a href="/pub-to-pdf" class="tool-card">
<i class="ph ph-book-open text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-book-open text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PUB to PDF</h3> <h3 class="font-semibold text-white">PUB to PDF</h3>
<p class="text-gray-400 text-sm">Publisher</p> <p class="text-gray-400 text-sm">Publisher</p>
</a> </a>
<a href="/src/pages/vsd-to-pdf.html" class="tool-card"> <a href="/vsd-to-pdf" class="tool-card">
<i class="ph ph-git-branch text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-git-branch text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">VSD to PDF</h3> <h3 class="font-semibold text-white">VSD to PDF</h3>
<p class="text-gray-400 text-sm">Visio</p> <p class="text-gray-400 text-sm">Visio</p>
</a> </a>
<a href="/src/pages/wpd-to-pdf.html" class="tool-card"> <a href="/wpd-to-pdf" class="tool-card">
<i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">WPD to PDF</h3> <h3 class="font-semibold text-white">WPD to PDF</h3>
<p class="text-gray-400 text-sm">WordPerfect</p> <p class="text-gray-400 text-sm">WordPerfect</p>
</a> </a>
<a href="/src/pages/wps-to-pdf.html" class="tool-card"> <a href="/wps-to-pdf" class="tool-card">
<i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-text text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">WPS to PDF</h3> <h3 class="font-semibold text-white">WPS to PDF</h3>
<p class="text-gray-400 text-sm">WPS Office</p> <p class="text-gray-400 text-sm">WPS Office</p>
</a> </a>
<a href="/src/pages/xml-to-pdf.html" class="tool-card"> <a href="/xml-to-pdf" class="tool-card">
<i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-code text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">XML to PDF</h3> <h3 class="font-semibold text-white">XML to PDF</h3>
<p class="text-gray-400 text-sm">XML data</p> <p class="text-gray-400 text-sm">XML data</p>
+23 -23
View File
@@ -111,117 +111,117 @@
<div <div
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"
> >
<a href="/src/pages/edit-pdf.html" class="tool-card"> <a href="/edit-pdf" class="tool-card">
<i class="ph ph-pencil-simple text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-pencil-simple text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Edit PDF</h3> <h3 class="font-semibold text-white">Edit PDF</h3>
<p class="text-gray-400 text-sm">Comprehensive editing</p> <p class="text-gray-400 text-sm">Comprehensive editing</p>
</a> </a>
<a href="/src/pages/compress-pdf.html" class="tool-card"> <a href="/compress-pdf" class="tool-card">
<i class="ph ph-lightning text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-lightning text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Compress PDF</h3> <h3 class="font-semibold text-white">Compress PDF</h3>
<p class="text-gray-400 text-sm">Reduce file size</p> <p class="text-gray-400 text-sm">Reduce file size</p>
</a> </a>
<a href="/src/pages/rotate-pdf.html" class="tool-card"> <a href="/rotate-pdf" class="tool-card">
<i class="ph ph-arrow-clockwise text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-arrow-clockwise text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Rotate PDF</h3> <h3 class="font-semibold text-white">Rotate PDF</h3>
<p class="text-gray-400 text-sm">Rotate pages</p> <p class="text-gray-400 text-sm">Rotate pages</p>
</a> </a>
<a href="/src/pages/crop-pdf.html" class="tool-card"> <a href="/crop-pdf" class="tool-card">
<i class="ph ph-crop text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-crop text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Crop PDF</h3> <h3 class="font-semibold text-white">Crop PDF</h3>
<p class="text-gray-400 text-sm">Trim pages</p> <p class="text-gray-400 text-sm">Trim pages</p>
</a> </a>
<a href="/src/pages/organize-pdf.html" class="tool-card"> <a href="/organize-pdf" class="tool-card">
<i class="ph ph-files text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-files text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Organize PDF</h3> <h3 class="font-semibold text-white">Organize PDF</h3>
<p class="text-gray-400 text-sm">Reorder pages</p> <p class="text-gray-400 text-sm">Reorder pages</p>
</a> </a>
<a href="/src/pages/delete-pages.html" class="tool-card"> <a href="/delete-pages" class="tool-card">
<i class="ph ph-trash text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-trash text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Delete Pages</h3> <h3 class="font-semibold text-white">Delete Pages</h3>
<p class="text-gray-400 text-sm">Remove pages</p> <p class="text-gray-400 text-sm">Remove pages</p>
</a> </a>
<a href="/src/pages/add-watermark.html" class="tool-card"> <a href="/add-watermark" class="tool-card">
<i class="ph ph-drop text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-drop text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Add Watermark</h3> <h3 class="font-semibold text-white">Add Watermark</h3>
<p class="text-gray-400 text-sm">Protect PDFs</p> <p class="text-gray-400 text-sm">Protect PDFs</p>
</a> </a>
<a href="/src/pages/header-footer.html" class="tool-card"> <a href="/header-footer" class="tool-card">
<i class="ph ph-paragraph text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-paragraph text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Header & Footer</h3> <h3 class="font-semibold text-white">Header & Footer</h3>
<p class="text-gray-400 text-sm">Add headers/footers</p> <p class="text-gray-400 text-sm">Add headers/footers</p>
</a> </a>
<a href="/src/pages/page-numbers.html" class="tool-card"> <a href="/page-numbers" class="tool-card">
<i class="ph ph-list-numbers text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-list-numbers text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Page Numbers</h3> <h3 class="font-semibold text-white">Page Numbers</h3>
<p class="text-gray-400 text-sm">Number pages</p> <p class="text-gray-400 text-sm">Number pages</p>
</a> </a>
<a href="/src/pages/background-color.html" class="tool-card"> <a href="/background-color" class="tool-card">
<i class="ph ph-palette text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-palette text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Background Color</h3> <h3 class="font-semibold text-white">Background Color</h3>
<p class="text-gray-400 text-sm">Change background</p> <p class="text-gray-400 text-sm">Change background</p>
</a> </a>
<a href="/src/pages/text-color.html" class="tool-card"> <a href="/text-color" class="tool-card">
<i class="ph ph-eyedropper text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-eyedropper text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Text Color</h3> <h3 class="font-semibold text-white">Text Color</h3>
<p class="text-gray-400 text-sm">Change text color</p> <p class="text-gray-400 text-sm">Change text color</p>
</a> </a>
<a href="/src/pages/invert-colors.html" class="tool-card"> <a href="/invert-colors" class="tool-card">
<i class="ph ph-circle-half text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-circle-half text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Invert Colors</h3> <h3 class="font-semibold text-white">Invert Colors</h3>
<p class="text-gray-400 text-sm">Dark mode</p> <p class="text-gray-400 text-sm">Dark mode</p>
</a> </a>
<a href="/src/pages/add-blank-page.html" class="tool-card"> <a href="/add-blank-page" class="tool-card">
<i class="ph ph-file-plus text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-plus text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Add Blank Page</h3> <h3 class="font-semibold text-white">Add Blank Page</h3>
<p class="text-gray-400 text-sm">Insert pages</p> <p class="text-gray-400 text-sm">Insert pages</p>
</a> </a>
<a href="/src/pages/reverse-pages.html" class="tool-card"> <a href="/reverse-pages" class="tool-card">
<i class="ph ph-sort-descending text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-sort-descending text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Reverse Pages</h3> <h3 class="font-semibold text-white">Reverse Pages</h3>
<p class="text-gray-400 text-sm">Reverse order</p> <p class="text-gray-400 text-sm">Reverse order</p>
</a> </a>
<a href="/src/pages/divide-pages.html" class="tool-card"> <a href="/divide-pages" class="tool-card">
<i class="ph ph-columns text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-columns text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Divide Pages</h3> <h3 class="font-semibold text-white">Divide Pages</h3>
<p class="text-gray-400 text-sm">Split into parts</p> <p class="text-gray-400 text-sm">Split into parts</p>
</a> </a>
<a href="/src/pages/remove-annotations.html" class="tool-card"> <a href="/remove-annotations" class="tool-card">
<i class="ph ph-eraser text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-eraser text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Remove Annotations</h3> <h3 class="font-semibold text-white">Remove Annotations</h3>
<p class="text-gray-400 text-sm">Clean comments</p> <p class="text-gray-400 text-sm">Clean comments</p>
</a> </a>
<a href="/src/pages/remove-blank-pages.html" class="tool-card"> <a href="/remove-blank-pages" class="tool-card">
<i class="ph ph-file-minus text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-minus text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Remove Blank Pages</h3> <h3 class="font-semibold text-white">Remove Blank Pages</h3>
<p class="text-gray-400 text-sm">Auto-clean</p> <p class="text-gray-400 text-sm">Auto-clean</p>
</a> </a>
<a href="/src/pages/extract-pages.html" class="tool-card"> <a href="/extract-pages" class="tool-card">
<i class="ph ph-squares-four text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-squares-four text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Extract Pages</h3> <h3 class="font-semibold text-white">Extract Pages</h3>
<p class="text-gray-400 text-sm">Pull out pages</p> <p class="text-gray-400 text-sm">Pull out pages</p>
</a> </a>
<a href="/src/pages/extract-images.html" class="tool-card"> <a href="/extract-images" class="tool-card">
<i class="ph ph-download-simple text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-download-simple text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Extract Images</h3> <h3 class="font-semibold text-white">Extract Images</h3>
<p class="text-gray-400 text-sm">Get images</p> <p class="text-gray-400 text-sm">Get images</p>
</a> </a>
<a href="/src/pages/extract-tables.html" class="tool-card"> <a href="/extract-tables" class="tool-card">
<i class="ph ph-table text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-table text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Extract Tables</h3> <h3 class="font-semibold text-white">Extract Tables</h3>
<p class="text-gray-400 text-sm">Export tables</p> <p class="text-gray-400 text-sm">Export tables</p>
</a> </a>
<a href="/src/pages/n-up-pdf.html" class="tool-card"> <a href="/n-up-pdf" class="tool-card">
<i class="ph ph-squares-four text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-squares-four text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">N-up PDF</h3> <h3 class="font-semibold text-white">N-up PDF</h3>
<p class="text-gray-400 text-sm">Multiple per page</p> <p class="text-gray-400 text-sm">Multiple per page</p>
</a> </a>
<a href="/src/pages/rotate-custom.html" class="tool-card"> <a href="/rotate-custom" class="tool-card">
<i class="ph ph-arrows-clockwise text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-arrows-clockwise text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Custom Rotation</h3> <h3 class="font-semibold text-white">Custom Rotation</h3>
<p class="text-gray-400 text-sm">Precise angles</p> <p class="text-gray-400 text-sm">Precise angles</p>
</a> </a>
<a href="/src/pages/pdf-to-greyscale.html" class="tool-card"> <a href="/pdf-to-greyscale" class="tool-card">
<i class="ph ph-palette text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-palette text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">To Greyscale</h3> <h3 class="font-semibold text-white">To Greyscale</h3>
<p class="text-gray-400 text-sm">Remove color</p> <p class="text-gray-400 text-sm">Remove color</p>
+12 -12
View File
@@ -119,64 +119,64 @@
<div <div
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"
> >
<a href="/src/pages/merge-pdf.html" class="tool-card"> <a href="/merge-pdf" class="tool-card">
<i class="ph ph-browsers text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-browsers text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Merge PDF</h3> <h3 class="font-semibold text-white">Merge PDF</h3>
<p class="text-gray-400 text-sm">Combine PDFs</p> <p class="text-gray-400 text-sm">Combine PDFs</p>
</a> </a>
<a href="/src/pages/split-pdf.html" class="tool-card"> <a href="/split-pdf" class="tool-card">
<i class="ph ph-scissors text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-scissors text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Split PDF</h3> <h3 class="font-semibold text-white">Split PDF</h3>
<p class="text-gray-400 text-sm">Separate pages</p> <p class="text-gray-400 text-sm">Separate pages</p>
</a> </a>
<a href="/src/pages/organize-pdf.html" class="tool-card"> <a href="/organize-pdf" class="tool-card">
<i class="ph ph-files text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-files text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Organize PDF</h3> <h3 class="font-semibold text-white">Organize PDF</h3>
<p class="text-gray-400 text-sm">Reorder pages</p> <p class="text-gray-400 text-sm">Reorder pages</p>
</a> </a>
<a href="/src/pages/extract-pages.html" class="tool-card"> <a href="/extract-pages" class="tool-card">
<i class="ph ph-squares-four text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-squares-four text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Extract Pages</h3> <h3 class="font-semibold text-white">Extract Pages</h3>
<p class="text-gray-400 text-sm">Pull out pages</p> <p class="text-gray-400 text-sm">Pull out pages</p>
</a> </a>
<a href="/src/pages/delete-pages.html" class="tool-card"> <a href="/delete-pages" class="tool-card">
<i class="ph ph-trash text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-trash text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Delete Pages</h3> <h3 class="font-semibold text-white">Delete Pages</h3>
<p class="text-gray-400 text-sm">Remove pages</p> <p class="text-gray-400 text-sm">Remove pages</p>
</a> </a>
<a href="/src/pages/divide-pages.html" class="tool-card"> <a href="/divide-pages" class="tool-card">
<i class="ph ph-columns text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-columns text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Divide Pages</h3> <h3 class="font-semibold text-white">Divide Pages</h3>
<p class="text-gray-400 text-sm">Split into parts</p> <p class="text-gray-400 text-sm">Split into parts</p>
</a> </a>
<a href="/src/pages/add-blank-page.html" class="tool-card"> <a href="/add-blank-page" class="tool-card">
<i class="ph ph-file-plus text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-plus text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Add Blank Page</h3> <h3 class="font-semibold text-white">Add Blank Page</h3>
<p class="text-gray-400 text-sm">Insert pages</p> <p class="text-gray-400 text-sm">Insert pages</p>
</a> </a>
<a href="/src/pages/reverse-pages.html" class="tool-card"> <a href="/reverse-pages" class="tool-card">
<i class="ph ph-sort-descending text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-sort-descending text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Reverse Pages</h3> <h3 class="font-semibold text-white">Reverse Pages</h3>
<p class="text-gray-400 text-sm">Reverse order</p> <p class="text-gray-400 text-sm">Reverse order</p>
</a> </a>
<a href="/src/pages/alternate-merge.html" class="tool-card"> <a href="/alternate-merge" class="tool-card">
<i class="ph ph-shuffle text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-shuffle text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Alternate Merge</h3> <h3 class="font-semibold text-white">Alternate Merge</h3>
<p class="text-gray-400 text-sm">Interleave PDFs</p> <p class="text-gray-400 text-sm">Interleave PDFs</p>
</a> </a>
<a href="/src/pages/combine-single-page.html" class="tool-card"> <a href="/combine-single-page" class="tool-card">
<i <i
class="ph ph-arrows-out-line-vertical text-4xl text-indigo-400 mb-2" class="ph ph-arrows-out-line-vertical text-4xl text-indigo-400 mb-2"
></i> ></i>
<h3 class="font-semibold text-white">Combine Pages</h3> <h3 class="font-semibold text-white">Combine Pages</h3>
<p class="text-gray-400 text-sm">Join pages</p> <p class="text-gray-400 text-sm">Join pages</p>
</a> </a>
<a href="/src/pages/compare-pdfs.html" class="tool-card"> <a href="/compare-pdfs" class="tool-card">
<i class="ph ph-git-diff text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-git-diff text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Compare PDFs</h3> <h3 class="font-semibold text-white">Compare PDFs</h3>
<p class="text-gray-400 text-sm">Find differences</p> <p class="text-gray-400 text-sm">Find differences</p>
</a> </a>
<a href="/src/pages/pdf-booklet.html" class="tool-card"> <a href="/pdf-booklet" class="tool-card">
<i class="ph ph-book-open text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-book-open text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF Booklet</h3> <h3 class="font-semibold text-white">PDF Booklet</h3>
<p class="text-gray-400 text-sm">Create booklet</p> <p class="text-gray-400 text-sm">Create booklet</p>
+10 -10
View File
@@ -111,52 +111,52 @@
<div <div
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"
> >
<a href="/src/pages/encrypt-pdf.html" class="tool-card"> <a href="/encrypt-pdf" class="tool-card">
<i class="ph ph-lock text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-lock text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Encrypt PDF</h3> <h3 class="font-semibold text-white">Encrypt PDF</h3>
<p class="text-gray-400 text-sm">Add password</p> <p class="text-gray-400 text-sm">Add password</p>
</a> </a>
<a href="/src/pages/decrypt-pdf.html" class="tool-card"> <a href="/decrypt-pdf" class="tool-card">
<i class="ph ph-lock-open text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-lock-open text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Decrypt PDF</h3> <h3 class="font-semibold text-white">Decrypt PDF</h3>
<p class="text-gray-400 text-sm">Remove password</p> <p class="text-gray-400 text-sm">Remove password</p>
</a> </a>
<a href="/src/pages/sign-pdf.html" class="tool-card"> <a href="/sign-pdf" class="tool-card">
<i class="ph ph-pen-nib text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-pen-nib text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Sign PDF</h3> <h3 class="font-semibold text-white">Sign PDF</h3>
<p class="text-gray-400 text-sm">Digital signature</p> <p class="text-gray-400 text-sm">Digital signature</p>
</a> </a>
<a href="/src/pages/remove-restrictions.html" class="tool-card"> <a href="/remove-restrictions" class="tool-card">
<i class="ph ph-link-break text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-link-break text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Remove Restrictions</h3> <h3 class="font-semibold text-white">Remove Restrictions</h3>
<p class="text-gray-400 text-sm">Unlock editing</p> <p class="text-gray-400 text-sm">Unlock editing</p>
</a> </a>
<a href="/src/pages/change-permissions.html" class="tool-card"> <a href="/change-permissions" class="tool-card">
<i class="ph ph-shield-check text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-shield-check text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Change Permissions</h3> <h3 class="font-semibold text-white">Change Permissions</h3>
<p class="text-gray-400 text-sm">Set permissions</p> <p class="text-gray-400 text-sm">Set permissions</p>
</a> </a>
<a href="/src/pages/sanitize-pdf.html" class="tool-card"> <a href="/sanitize-pdf" class="tool-card">
<i class="ph ph-broom text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-broom text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Sanitize PDF</h3> <h3 class="font-semibold text-white">Sanitize PDF</h3>
<p class="text-gray-400 text-sm">Remove metadata</p> <p class="text-gray-400 text-sm">Remove metadata</p>
</a> </a>
<a href="/src/pages/flatten-pdf.html" class="tool-card"> <a href="/flatten-pdf" class="tool-card">
<i class="ph ph-stack text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-stack text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Flatten PDF</h3> <h3 class="font-semibold text-white">Flatten PDF</h3>
<p class="text-gray-400 text-sm">Merge layers</p> <p class="text-gray-400 text-sm">Merge layers</p>
</a> </a>
<a href="/src/pages/linearize-pdf.html" class="tool-card"> <a href="/linearize-pdf" class="tool-card">
<i class="ph ph-gauge text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-gauge text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Linearize PDF</h3> <h3 class="font-semibold text-white">Linearize PDF</h3>
<p class="text-gray-400 text-sm">Optimize for web</p> <p class="text-gray-400 text-sm">Optimize for web</p>
</a> </a>
<a href="/src/pages/remove-metadata.html" class="tool-card"> <a href="/remove-metadata" class="tool-card">
<i class="ph ph-file-x text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-file-x text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">Remove Metadata</h3> <h3 class="font-semibold text-white">Remove Metadata</h3>
<p class="text-gray-400 text-sm">Clean properties</p> <p class="text-gray-400 text-sm">Clean properties</p>
</a> </a>
<a href="/src/pages/pdf-layers.html" class="tool-card"> <a href="/pdf-layers" class="tool-card">
<i class="ph ph-stack-simple text-4xl text-indigo-400 mb-2"></i> <i class="ph ph-stack-simple text-4xl text-indigo-400 mb-2"></i>
<h3 class="font-semibold text-white">PDF Layers</h3> <h3 class="font-semibold text-white">PDF Layers</h3>
<p class="text-gray-400 text-sm">Manage layers</p> <p class="text-gray-400 text-sm">Manage layers</p>
+5 -4
View File
@@ -240,7 +240,8 @@ function processFileForLanguage(
defaultLink.href = buildUrl('', pagePath); defaultLink.href = buildUrl('', pagePath);
document.head.appendChild(defaultLink); document.head.appendChild(defaultLink);
const canonicalUrl = buildUrl(lang, pagePath); const localizedUrl = buildUrl(lang, pagePath);
const canonicalUrl = buildUrl('', pagePath);
let canonical = document.querySelector('link[rel="canonical"]'); let canonical = document.querySelector('link[rel="canonical"]');
if (!canonical) { if (!canonical) {
canonical = document.createElement('link'); canonical = document.createElement('link');
@@ -250,15 +251,15 @@ function processFileForLanguage(
canonical.href = canonicalUrl; canonical.href = canonicalUrl;
const ogUrl = document.querySelector('meta[property="og:url"]'); const ogUrl = document.querySelector('meta[property="og:url"]');
if (ogUrl) ogUrl.content = canonicalUrl; if (ogUrl) ogUrl.content = localizedUrl;
const twitterUrl = document.querySelector('meta[name="twitter:url"]'); const twitterUrl = document.querySelector('meta[name="twitter:url"]');
if (twitterUrl) twitterUrl.content = canonicalUrl; if (twitterUrl) twitterUrl.content = localizedUrl;
injectOrganizationLd(document); injectOrganizationLd(document);
const localizedToolName = resolveToolName(translationKey, tools); const localizedToolName = resolveToolName(translationKey, tools);
if (localizedToolName) { if (localizedToolName) {
injectToolBreadcrumb(document, lang, localizedToolName, canonicalUrl); injectToolBreadcrumb(document, lang, localizedToolName, localizedUrl);
} }
const links = document.querySelectorAll('a[href]'); const links = document.querySelectorAll('a[href]');
+12 -18
View File
@@ -92,31 +92,26 @@ function generateSitemap() {
for (const pageName of htmlFiles) { for (const pageName of htmlFiles) {
const priority = getPriority(pageName); const priority = getPriority(pageName);
const url = buildUrl('en', pageName);
const lastmod = getLastMod('en', pageName);
for (const lang of languages) { sitemap += ` <url>
const url = buildUrl(lang, pageName);
const lastmod = getLastMod(lang, pageName);
sitemap += ` <url>
<loc>${url}</loc> <loc>${url}</loc>
<lastmod>${lastmod}</lastmod> <lastmod>${lastmod}</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>${priority}</priority> <priority>${priority}</priority>
`; `;
// Add hreflang alternates for all languages for (const altLang of languages) {
for (const altLang of languages) { const altUrl = buildUrl(altLang, pageName);
const altUrl = buildUrl(altLang, pageName); sitemap += ` <xhtml:link rel="alternate" hreflang="${altLang}" href="${altUrl}"/>
sitemap += ` <xhtml:link rel="alternate" hreflang="${altLang}" href="${altUrl}"/>
`;
}
// Add x-default pointing to English
const defaultUrl = buildUrl('en', pageName);
sitemap += ` <xhtml:link rel="alternate" hreflang="x-default" href="${defaultUrl}"/>
</url>
`; `;
} }
const defaultUrl = buildUrl('en', pageName);
sitemap += ` <xhtml:link rel="alternate" hreflang="x-default" href="${defaultUrl}"/>
</url>
`;
} }
sitemap += `</urlset> sitemap += `</urlset>
@@ -128,9 +123,8 @@ function generateSitemap() {
const publicSitemapPath = path.resolve(__dirname, '../public/sitemap.xml'); const publicSitemapPath = path.resolve(__dirname, '../public/sitemap.xml');
fs.writeFileSync(publicSitemapPath, sitemap); fs.writeFileSync(publicSitemapPath, sitemap);
const urlCount = htmlFiles.length * languages.length;
console.log( console.log(
`✅ Sitemap generated with ${urlCount} URLs (${htmlFiles.length} pages × ${languages.length} languages)` `✅ Sitemap generated with ${htmlFiles.length} canonical URLs (${languages.length} hreflang alternates each)`
); );
} }
+3 -7
View File
@@ -69,11 +69,9 @@ function findAll(html, regex) {
function expectedCanonicalForFile(rel) { function expectedCanonicalForFile(rel) {
const parts = rel.split('/'); const parts = rel.split('/');
const fileName = parts.pop(); const fileName = parts.pop();
const langPrefix = parts.length > 0 ? parts.join('/') : '';
const baseName = fileName.replace(/\.html$/, ''); const baseName = fileName.replace(/\.html$/, '');
const slug = baseName === 'index' ? '' : baseName; const slug = baseName === 'index' ? '' : baseName;
const segments = [SITE_URL]; const segments = [SITE_URL];
if (langPrefix) segments.push(langPrefix);
if (slug) segments.push(slug); if (slug) segments.push(slug);
return segments.join('/').replace(/\/+$/, '') || SITE_URL; return segments.join('/').replace(/\/+$/, '') || SITE_URL;
} }
@@ -230,11 +228,9 @@ function auditSitemap() {
.filter((d) => fs.statSync(path.join(LOCALES_DIR, d)).isDirectory()); .filter((d) => fs.statSync(path.join(LOCALES_DIR, d)).isDirectory());
for (const lang of expectedLocales) { for (const lang of expectedLocales) {
if (lang === 'en') continue; if (lang === 'en') continue;
const sample = locs.find( const hreflangPattern = new RegExp(`hreflang="${lang}"`);
(l) => l.includes(`/${lang}/`) || l.endsWith(`/${lang}`) if (!hreflangPattern.test(xml)) {
); warn('sitemap', `sitemap has no hreflang entry for locale "${lang}"`);
if (!sample) {
warn('sitemap', `sitemap has no entry for locale "${lang}"`);
} }
} }
} }
+8
View File
@@ -112,6 +112,14 @@ async function changePermissions() {
(document.getElementById('new-owner-password') as HTMLInputElement) (document.getElementById('new-owner-password') as HTMLInputElement)
?.value || ''; ?.value || '';
if (newUserPassword && !newOwnerPassword) {
showAlert(
'Owner Password Required',
'An owner password is required when setting permissions'
);
return;
}
const inputPath = '/input.pdf'; const inputPath = '/input.pdf';
const outputPath = '/output.pdf'; const outputPath = '/output.pdf';
let qpdf: QpdfInstanceExtended; let qpdf: QpdfInstanceExtended;
+6 -2
View File
@@ -199,16 +199,20 @@
<label <label
for="new-owner-password" for="new-owner-password"
class="block mb-2 text-sm font-medium text-gray-300" class="block mb-2 text-sm font-medium text-gray-300"
>New Owner Password</label >New Owner Password
<span class="text-red-400" aria-hidden="true">*</span></label
> >
<input <input
type="password" type="password"
id="new-owner-password" id="new-owner-password"
placeholder="Password for permissions" placeholder="Password for permissions"
required
aria-required="true"
class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5"
/> />
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-gray-500 mt-1">
Leave both passwords empty to decrypt the PDF. Required when setting permissions or a user password. Leave all
fields empty to decrypt the PDF instead.
</p> </p>
</div> </div>