跳到正文
DEV Community· DenisKot·· 4 小時前AI 評分60

DenisKot 打造 Uttily,提供 546 個不會上傳檔案的網頁工具

I built 546 web tools that never upload your files

AI 導讀

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:

  1. a ToolMeta entry (slug, title, SEO title),
  2. a widget component: the markup plus an inline <script> with the real logic,
  3. 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 the load event, 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:

Questions about the setup are welcome in the comments.

來源:DEV Community · dev.to