Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,21 @@ jobs:
- run: npm run lint
- run: npm test
- run: npm run build

e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-results
path: test-results/
retention-days: 7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,7 @@ vite.config.ts.timestamp-*

# Wrangler local state
.wrangler/

# Playwright
test-results/
playwright-report/
47 changes: 33 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ Compressly is a **privacy-first file compression tool** that lets you reduce fil

## ✨ Features

- **🎚️ Compression Level Control** — Choose how aggressively to compress, from lossless to maximum reduction.
- **🎯 Target File Size** — Set an exact target size and let Compressly do the math for you.
- **🔒 Privacy First** — Files are processed locally or on the edge. Nothing is stored or logged on a remote server.
- **⚡ Fast & Lightweight** — Built with Vite and TypeScript for a snappy, modern experience.
- **☁️ Edge-Powered API** — Backend API runs on Cloudflare Workers for low-latency processing close to you.
- **🖥️ Clean UI** — A minimal, distraction-free interface designed for creators.
- **🎚️ Compression levels and exact targets** — Pick a level, or set a target size per file. One-click presets for WhatsApp (16 MB), Discord (10 MB), email (25 MB) and form uploads (200 KB / 100 KB).
- **🖼️ Images** — JPG, PNG, WebP, BMP and iPhone HEIC in; same format, JPG, WebP or AVIF out. Optional max dimensions. Before/after comparison slider.
- **📄 PDFs** — "Keep text selectable" re-compresses only the images inside, or "Smallest size" turns pages into images.
- **🎬 Video and audio** — MP4, WebM or GIF output, resolution, remove audio, trim. Audio becomes MP3.
- **🎞️ GIF and SVG** — Animated GIFs keep every frame (gifsicle); SVGs are optimized with SVGO.
- **🔒 Private by design** — Everything runs in your browser. Photo location and camera data is removed unless you choose to keep it.
- **📱 Installable app** — Works offline once loaded; on Android, share files to Compressly from any app.
- **✨ Comfortable to use** — Paste or drop whole folders, cancel any file, per-file options, remembered settings, dark mode.

---

Expand Down Expand Up @@ -72,10 +74,13 @@ The app will be available at `http://localhost:3000` by default.
### Checks

```bash
npm run lint # type-check
npm test # unit tests
npm run lint # type-check
npm test # unit tests
npm run test:e2e # browser tests against the production build (Playwright)
```

Run `npx playwright install chromium` once before the browser tests.

### Building for Production

```bash
Expand All @@ -97,7 +102,16 @@ npx wrangler deploy

With Workers Builds (Git integration), use `npm run build` as the build command and `npx wrangler deploy` (or `npx wrangler versions upload`) as the deploy command.

To run the production setup locally, including the API and KV stats:
Use `npx wrangler deploy` rather than `npx wrangler versions upload`: the stats counter is a Durable Object, and the first deploy has to apply its migration, which version uploads can't do.

Optional build-time environment variables:

| Variable | Purpose |
| --- | --- |
| `VITE_SITE_URL` | Public URL used in canonical links, `og:image` and `sitemap.xml` (default: `https://compressly.harshitks2203.workers.dev`) |
| `CF_ANALYTICS_TOKEN` | Enables Cloudflare Web Analytics |

To run the production setup locally, including the API and stats counter:

```bash
npm run build
Expand All @@ -112,11 +126,15 @@ npx wrangler dev
compressly/
├── docs/
│ └── screenshots/ # README images (not deployed)
├── worker/ # Cloudflare Worker: /api/* routes
├── worker/ # Cloudflare Worker: /api/* routes, stats Durable Object
├── tests/e2e/ # Playwright browser tests
├── scripts/ # Generates the PWA icons and og-image.png
├── public/ # Static assets
├── src/
│ ├── components/ # UI components
│ └── lib/ # Compression logic, loaded on demand per file type
│ ├── hooks/ # Settings and theme state
│ ├── lib/ # Compression logic, loaded on demand per file type
│ └── sw.ts # Service worker (offline, caching, share target)
├── index.html # App entry point
├── vite.config.ts # Vite configuration
├── wrangler.toml # Cloudflare Worker config
Expand All @@ -129,9 +147,10 @@ compressly/

Compressly is built with privacy as a core principle:

- **No file storage** — Your files are never persisted on any server.
- **No tracking** — No analytics or telemetry are attached to your files.
- **Edge processing** — API functions run on Cloudflare's edge network, meaning processing happens ephemerally and close to you.
- **Files never leave your device** — All compression runs in the browser (WebAssembly and web workers). No file is uploaded, stored or logged.
- **Metadata removed by default** — Re-encoded photos drop EXIF data such as GPS location, unless "Keep photo metadata" is on.
- **Minimal network use** — The only requests with user activity are an anonymous count of files and bytes saved for the public stats. Fonts and libraries are self-hosted; the FFmpeg core (~31 MB, too large for Cloudflare's 25 MiB file limit) is fetched from unpkg only when you add a video or audio file, then cached.
- **Optional analytics** — If `CF_ANALYTICS_TOKEN` is set at build time, Cloudflare Web Analytics counts page views. It uses no cookies and never sees your files.

You can verify this by inspecting the source code in `worker/` and `src/`.

Expand Down
50 changes: 30 additions & 20 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,50 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Basic SEO -->
<title>Compressly – Premium File Compression for Creators</title>
<meta name="title" content="Compressly – Premium File Compression">
<meta name="description" content="Shrink your files, keep the magic. Compressly delivers premium file compression for creators who care about quality. Optimize images, videos, and assets without losing detail.">
<meta name="keywords" content="Compressly, file compression, image compression, video compression, lossless compression, creator tools, optimize media, reduce file size">
<!-- Page-specific values are filled in by the landing-pages plugin in vite.config.ts -->
<title>__PAGE_TITLE__</title>
<meta name="description" content="__PAGE_DESCRIPTION__">
<meta name="author" content="Compressly">
<meta name="robots" content="index, follow">

<link rel="canonical" href="__PAGE_URL__">

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:title" content="Compressly – Shrink Your Files, Keep the Magic">
<meta property="og:description" content="Premium compression for creators who care about quality. Reduce file size while preserving the details that matter.">
<meta property="og:title" content="__PAGE_TITLE__">
<meta property="og:description" content="__PAGE_DESCRIPTION__">
<meta property="og:site_name" content="Compressly">
<meta property="og:url" content="https://compressly.harshitks2203.workers.dev/">

<meta property="og:url" content="__PAGE_URL__">
<meta property="og:image" content="__SITE_URL__/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Compressly – Shrink Your Files, Keep the Magic">
<meta name="twitter:description" content="Premium compression for creators who care about quality.">

<meta name="twitter:title" content="__PAGE_TITLE__">
<meta name="twitter:description" content="__PAGE_DESCRIPTION__">
<meta name="twitter:image" content="__SITE_URL__/og-image.png">

<!-- App / Product -->
<meta name="application-name" content="Compressly">
<meta name="apple-mobile-web-app-title" content="Compressly">
<meta name="theme-color" content="#fefae0">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23606c38'/%3E%3Ctext x='16' y='23' font-family='sans-serif' font-size='20' font-weight='700' fill='%23fefae0' text-anchor='middle'%3EC%3C/text%3E%3C/svg%3E">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">

<!-- Fonts: loaded here rather than via CSS @import so the request starts immediately -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" crossorigin href="https://fonts.googleapis.com/css2?family=Nunito+Sans:opsz,wght@6..12,300..700&display=swap">
<!-- Apply the saved theme before first paint to avoid a light flash in dark mode. -->
<script>
(function () {
var pref = "system";
try { pref = JSON.parse(localStorage.getItem("compressly:theme")) || "system"; } catch (e) {}
var dark = pref === "dark" || (pref === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.dataset.theme = dark ? "dark" : "light";
if (dark) document.querySelector('meta[name="theme-color"]').setAttribute("content", "#1a1e10");
})();
</script>
</head>
<body>
<div id="root"></div>
<!-- Static content for search engines and no-JS visitors; React replaces it. -->
<div id="root">__PAGE_CONTENT__</div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

Loading
Loading