DenisKot 打造 Uttily,提供 546 個不會上傳檔案的網頁工具
I built 546 web tools that never upload your files
DenisKot 打造了 Uttily,提供分佈於 29 個網站的 546 個網頁工具,所有檔案處理都在用戶裝置的瀏覽器內完成,沒有後端。PDF 合併、影片壓縮、背景移除和音訊轉錄等功能均在瀏覽器執行;頁面載入後,多數工具可離線使用。各網站以 Astro 建置並共用 UI 層,工具內容提供 11 種語言版本。
Most "free online" converters work the same way. You upload your file, a server processes it, and you download the result. That file might be a passport scan, a contract or a private video, and now it sits on someone else's server.
Browsers are fast enough now that this round trip often isn't needed. So I built Uttily: 546 tools across 29 sites, and none of them has a backend. Merging PDFs, compressing video, removing backgrounds and transcribing audio all happen on your own device.
This post covers how it works.
The rule: no server, ever
The whole project rests on one constraint: all processing happens in the browser (JavaScript, Canvas or WebAssembly). No uploads, no API calls with user data, no backend.
That constraint helps in three ways:
- Privacy is a fact, not a promise. You can open DevTools → Network and see that your file never leaves the page.
- Hosting is almost free. Every site is static HTML on Cloudflare Pages. A traffic spike costs nothing.
- It works offline. Once the page has loaded, most tools keep working without a connection.
What runs in the browser
| Task | Library |
|---|---|
| Merge, split, sign and redact PDFs |
pdf-lib, pdfjs-dist
|
| Convert, trim and compress video |
@ffmpeg/ffmpeg (ffmpeg.wasm) |
| Remove image backgrounds |
@imgly/background-removal (ONNX model) |
| Speech to text, translation |
@huggingface/transformers (Whisper, Opus-MT) |
| OCR | tesseract.js |
| HEIC → JPG | heic2any |
| ZIP / TAR |
fflate plus a small hand-written TAR writer |
| Hashing, encryption | crypto.subtle |
A few years ago, several of these would have needed a GPU server. Now they need a tab.
Architecture: one monorepo, 29 sites
packages/shared-ui/ layouts, SEO, i18n, client helpers
projects/pdf-tools/ → pdf.uttily.com
projects/image-tools/ → image.uttily.com
projects/media-tools/ → media.uttily.com
...
Each project is an independent Astro site with its own subdomain. They all share shared-ui, so a tool page is mostly:
- a
ToolMetaentry (slug, title, SEO title), - a widget component: the markup plus an inline
<script>with the real logic, - a page that puts the widget inside a shared
ToolLayout.
The layout adds everything else: breadcrumbs, FAQ schema, related tools, a favorites star, keyboard shortcuts, "Copy link" with the widget's state in the URL, and a "Copy image" button next to every image download.
The helpers that made 546 tools manageable
Most tools follow the same pattern: get a file, process it, return a file. So the shared client library handles input and output once:
import { wireDrop, downloadBlob, withExtension } from "@tools/shared-ui/lib/client";
wireDrop(dropzone, async (files) => {
const out = await process(files[0]);
downloadBlob(out, withExtension(files[0].name, "jpg"));
});
wireDrop handles drag and drop, the file picker, Ctrl+V paste, PWA "Open with" launches, and files passed in from another tool. Because downloadBlob emits an event, the layout can show a "Continue with…" bar. After converting HEIC → JPG, one click sends the result to the compressor, with no new upload or download.
Loading big engines without slowing the page
ffmpeg.wasm is around 30 MB, and the background-removal model is similar. Loading them up front would ruin Core Web Vitals. So:
- the page and widget load first and are interactive straight away,
-
preloadAfterLoad()warms the engine after theloadevent, when the browser is idle, - it skips preloading when the user has Data Saver turned on.
When someone actually drops a file, the engine is usually already in the cache.
Translation without making widgets translatable
All tools come in 11 languages. The trick was deciding what not to translate: widget buttons and scripts stay in English, and widget behavior never depends on display text (state lives in data-* attributes). Only the content is translated: title, guide, steps and FAQ, stored as one JSON file per language per tool.
A single dynamic route ([lang]/[...slug].astro) finds every widget and translation with import.meta.glob, and the build fails if any combination is missing. That check has caught many mistakes.
What I'd tell someone starting this
- Pick one hard constraint and design around it. "No server" made hundreds of later decisions for me.
- Put the boring parts in a shared layer early. Tool number 500 took minutes because the layout already did everything except the actual processing.
- Let the build enforce your rules. Missing translations, overly long SEO titles and broken cross-links all fail CI.
You can try it at uttily.com. Open the Network tab, drop a file into any of these, and watch nothing upload:
- PDF: merge, compress, sign, redact
- Images: HEIC to JPG, compress, remove background, blur faces
- Video & audio: compress video, video to GIF, transcribe audio, auto subtitles
- Files: ZIP files, extract any archive, detect file type
Questions about the setup are welcome in the comments.
來源:DEV Community · dev.to