Wenlan
Wenlan is a source-backed AI knowledge base and LLM wiki for AI work: organize documents and decisions into pages you can find, inspect, and review.
Free online tools for PDFs, images, text and web work: convert and compress files, edit PDFs, count words, compare text, format JSON and make QR codes.
ProductivityTypeScript · JavaScriptGitHub @adamkhan4all-altOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗React + TypeScript + Vite + React Router + Tailwind CSS. No backend yet — processing is mocked so the full UI/UX can be exercised end-to-end.
npm install
npm run dev # dev server
npm run build # production build (tsc -b && vite build)
npm run test # vitest — registry + routing tests
ToolDefinition (src/types/tool.ts)The single data contract every tool conforms to: id, name, slug,
category, description, acceptedFormats, maxFileSize,
processingMode ("CLIENT" | "SERVER"), endpoint, seo metadata
(title, metaDescription, h1, keywords), faq, relatedTools, plus
the fields the UI needs (options, howItWorks, features, etc).
processingMode: "CLIENT" — the tool runs entirely in the browser; endpoint is null.processingMode: "SERVER" — the tool uploads to endpoint for processing.Today compress-image, jpg-to-png, and png-to-jpg are marked CLIENT
(a canvas-based implementation is a natural fit); the rest are SERVER.
Neither mode has real processing wired up yet — both still go through the
same mocked pipeline — but the flag is threaded through the framework
(e.g. PrivacyNotice copy changes based on it) so swapping in the real
implementation later doesn't require touching the framework again.
ToolRegistry (src/lib/toolRegistry.ts)Centralized, validated lookup layer. Built once from
TOOL_DEFINITIONS (src/data/tools.ts) and exported as the toolRegistry
singleton. Throws at module load if two tools share an id or slug, so a
data-entry mistake fails immediately instead of producing a broken route.
API: getAll(), getById(id), getBySlug(slug), getByCategory(category),
getRelated(tool), search(query), has(slug), count().
The ToolRegistry class is also exported directly so tests can build
isolated registries from fixtures without touching the singleton.
src/App.tsx defines one dynamic route, /tools/:slug, which resolves
through src/pages/ToolPage.tsx (ToolPageRoute) — the only place the
router talks to tool data. It looks up the slug in toolRegistry, and
either renders the generic ToolPage framework component with the
resolved ToolDefinition, or redirects to /tools if the slug is unknown.
Adding tool #11 requires exactly one change: append a ToolDefinition
object to TOOL_DEFINITIONS in src/data/tools.ts. No new route, no new
page component, no registry code, no UI code.
src/components/tools/ + src/components/ui/)| Component | Responsibility |
|---|---|
ToolPage | Generic page composition — the only tool-page layout that exists |
ToolHeader | Breadcrumb + H1 + description |
FileDropZone | Drag-and-drop / file-picker upload surface |
FileList | Renders selected files + aggregate size for multi-file tools |
FilePreview | Thumbnail for images, type-glyph badge otherwise |
ToolOptions | Schema-driven select/radio options panel |
ProcessButton | Primary per-tool CTA, sticky on mobile once enabled |
ProcessingState | Uploading (determinate) / Processing (indeterminate) UI |
ProgressIndicator | Determinate + indeterminate progress bar primitives |
ResultCard | Completed-state card (uses DownloadButton) |
DownloadButton | Standalone download CTA |
ErrorState / inline error block | Error-state UI with retry |
PrivacyNotice | Trust copy, adapts to processingMode |
AdSlot | 5 reusable ad placeholder variants |
ToolInstructions | "How it works" numbered steps |
FAQ | Accordion, per-tool Q&A |
RelatedTools | Pulls related tools via toolRegistry.getRelated |
No tool page contains bespoke markup — every tool renders through the same
ToolPage component tree, differing only by the ToolDefinition passed in.
npm run test)src/lib/__tests__/toolRegistry.test.ts — registry contents, lookup by
slug/category, related-tool resolution (including unknown-id handling),
search, SEO/FAQ completeness per tool, and constructor-level duplicate-id/
slug validation.src/__tests__/routing.test.tsx — renders <App> at /, /tools, every
/tools/:slug route (asserting the correct H1 per tool), an unknown tool
slug (redirects to /tools), and an unrelated unknown path (404 page);
also asserts every tool's process button renders.//tools/tools/pdf-to-word/tools/word-to-pdf/tools/jpg-to-pdf/tools/pdf-to-jpg/tools/compress-pdf/tools/merge-pdf/tools/compress-image/tools/image-to-text/tools/jpg-to-png/tools/png-to-jpgThis is a client-rendered SPA (Vite + React Router, createRoot, no SSR
framework), which is fine for Googlebot — it executes JavaScript — but is a
real gap for crawlers/bots that don't (most social link-preview scrapers,
some simpler SEO tools). The SEO layer below has two halves that work
together: a runtime half for real visitors and JS-executing crawlers, and a
build-time prerender for everyone else.
Every one of the 10 tool routes (plus / and /tools) now has, driven
entirely by its own ToolDefinition (src/data/tools.ts — nothing shared/
boilerplate):
This is the beginning of the README. Read the rest at the original. Read the full README ↗