Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

IT-Tools — Administrator & Developer Guide

Generated June 2026 from CorentinTh/it-tools at d505845

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the deep, source-based reference for operating, self-hosting, configuring, and extending IT-Tools — the open-source collection of ~100 client-side developer utilities by Corentin Thomasset. It is written for administrators deploying IT-Tools on their own infrastructure, and for developers who want to understand the architecture, add tools, or contribute.

If you are an end user looking for how to use the tools, see the User Guide.


Table of Contents

  1. Executive Summary
  2. Architecture
  3. Repository Structure
  4. Requirements
  5. Configuration Reference
  6. Deployment
  7. Operations
  8. Security Analysis
  9. Building from Source & Developer Workflow
  10. Extending: Adding a New Tool
  11. Internals Deep Dive
  12. Code Quality & Conventions
  13. Testing Strategy
  14. Potential Improvements
  15. Quick Reference

1. Executive Summary

IT-Tools is a Vue 3 single-page application that bundles around one hundred small, self-contained utilities behind one searchable interface. Its defining architectural property is that it is 100% client-side: there is no backend service, no database, no application server, and no API. The deployable artifact is a directory of static files (HTML, JS, CSS, assets) served by a plain web server.

Key operational facts:

Property Value
Type Static single-page web application (SPA)
Framework Vue 3 + TypeScript
UI library Naive UI + a small custom c-* component kit, UnoCSS for atomic styling
Build tool Vite 4 (via pnpm)
Runtime backend None — static files only
Production server nginx (in the official Docker image)
Default container port 80 (commonly published as host 8080)
Persistence None server-side; user state lives in the browser (local storage / URL)
Analytics Plausible, disabled by default; privacy-friendly when enabled
Configuration Build-time environment variables (baked into the bundle)
License GNU GPLv3
Analyzed version 2024.10.22-7ca5933 (package.json), commit d505845

What this means for administrators: deploying IT-Tools is deploying a static site. There is nothing to scale, no stateful component to back up, and the security surface is minimal (no server-side input handling, no secrets at runtime). The most important thing to understand is that all configuration is applied at build time — changing analytics, the base path, or banners requires rebuilding the image, not setting an environment variable on the running container. This is covered in detail in §5.


2. Architecture

2.1 High-level picture

                          Browser (the entire runtime)
   ┌───────────────────────────────────────────────────────────────┐
   │  Vue 3 SPA                                                      │
   │                                                                 │
   │   main.ts ──> createApp(App)                                    │
   │     ├─ Pinia            (state: style, tools, command-palette)  │
   │     ├─ vue-router        (routes auto-generated from tools)     │
   │     ├─ vue-i18n          (locale messages, persisted)           │
   │     ├─ naive-ui          (component library)                    │
   │     ├─ plausible plugin  (no-op unless enabled)                 │
   │     └─ vue-shadow-dom                                           │
   │                                                                 │
   │   App.vue ──> <layout> ──> <RouterView/>                        │
   │                  │                                              │
   │                  ├─ base.layout  (sidebar + top bar + home)     │
   │                  └─ tool.layout  (per-tool header + content)    │
   │                                                                 │
   │   Tool registry:  src/tools/<name>/index.ts -> defineTool()     │
   │     aggregated in src/tools/index.ts -> toolsByCategory[]       │
   │                                                                 │
   │   State persisted in: localStorage + URL query params           │
   │   Service Worker (PWA): offline cache, auto-update              │
   └───────────────────────────────────────────────────────────────┘
                          ▲
                          │ static files over HTTP(S)
   ┌──────────────────────┴───────────────────────────────────────┐
   │  nginx:stable-alpine  — serves /usr/share/nginx/html          │
   │  SPA fallback: try_files $uri $uri/ /index.html               │
   └───────────────────────────────────────────────────────────────┘

There is no tier below nginx. The web server's only job is to serve the compiled assets and route all unknown paths back to index.html so the client-side router can handle them.

2.2 The tool registry — the core architectural pattern

The single most important pattern in the codebase is the tool registry. Each tool is a self-contained folder under src/tools/<tool-name>/ whose index.ts exports a tool descriptor built with the defineTool() factory:

// src/tools/uuid-generator/index.ts
import { Fingerprint } from '@vicons/tabler';
import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';

export const tool = defineTool({
  name: translate('tools.uuid-generator.title'),
  path: '/uuid-generator',
  description: translate('tools.uuid-generator.description'),
  keywords: ['uuid', 'v4', 'random', 'id', /* ... */],
  component: () => import('./uuid-generator.vue'),  // lazy-loaded
  icon: Fingerprint,
  redirectFrom: ['/old-path'],   // optional legacy redirects
  createdAt: new Date('2024-01-31'),  // optional; drives the "New" badge
});

The defineTool factory (src/tools/tool.ts) is deliberately tiny — it only computes the isNew flag:

export function defineTool(tool: WithOptional<Tool, 'isNew'>) {
  const isNew = tool.createdAt ? isAfter(tool.createdAt, subWeeks(new Date(), 2)) : false;
  return { isNew, ...tool };
}

So a tool is "new" if its createdAt is within the last two weeks.

All tools are imported and grouped into categories in src/tools/index.ts:

export const toolsByCategory: ToolCategory[] = [
  { name: 'Crypto',    components: [tokenGenerator, hashText, bcrypt, /* ... */] },
  { name: 'Converter', components: [dateTimeConverter, baseConverter, /* ... */] },
  // ... 10 categories total
];

export const tools = toolsByCategory.flatMap(({ components }) => components);
export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) =>
  components.map(tool => ({ category: name, ...tool })),
);

Important

This registry is the backbone of the whole app. The router, the sidebar menu, the home grid, the favorites system, and the command palette are all derived from toolsByCategory/tools. Adding a tool to the registry automatically wires it into navigation, search, routing, and favorites — there is no separate route table or menu definition to maintain.

2.3 Routing

src/router.ts builds the route table programmatically from the registry:

const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
  path, name, component,
  meta: { isTool: true, layout: layouts.toolLayout, name, ...config },
}));

const toolsRedirectRoutes = tools
  .filter(({ redirectFrom }) => redirectFrom?.length)
  .flatMap(({ path, redirectFrom }) =>
    redirectFrom!.map(src => ({ path: src, redirect: path })));

const router = createRouter({
  history: createWebHistory(config.app.baseUrl),
  routes: [
    { path: '/', name: 'home', component: HomePage },
    { path: '/about', name: 'about', component: () => import('./pages/About.vue') },
    ...toolsRoutes,
    ...toolsRedirectRoutes,
    ...(config.app.env === 'development' ? demoRoutes : []),  // UI lab, dev only
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  ],
});

Notes:

  • History mode is createWebHistory with a configurable base URL (config.app.baseUrl). This is why nginx needs the SPA fallback — deep links like /jwt-parser must resolve to index.html.
  • Redirects are first-class: a tool can declare redirectFrom: ['/old-name'] to preserve old bookmarks (e.g. /cypher → /encryption, /hash → /hash-text).
  • A dev-only "UI lab" (/c-lib, the demoRoutes) is mounted only when config.app.env === 'development'.

2.4 State management (Pinia)

There are three Pinia stores:

Store File Responsibility Persistence
style src/stores/style.store.ts Dark theme, small-screen detection, menu collapse useDark() + useStorage('isMenuCollapsed')
tools src/tools/tools.store.ts Favorites, i18n-resolved tool list, categories, new tools useStorage('favoriteToolsName')
command-palette src/modules/command-palette/command-palette.store.ts Fuzzy search over tools + quick actions none

The tools store re-projects the static registry through i18n (resolving titles/descriptions for the active locale), groups by category, and exposes favorite management:

favoriteToolsName = useStorage('favoriteToolsName', [])   // string[] of tool paths
addToolToFavorites / removeToolFromFavorites / isToolFavorite / updateFavoriteTools
newTools = tools.filter(({ isNew }) => isNew)

The command-palette store builds a searchable list of all tools plus actions (Random tool, Toggle dark mode, GitHub repository, Report a bug, About) and runs a Fuse.js fuzzy search weighted toward the tool name:

useFuzzySearch({
  search: searchPrompt,
  data: searchOptions,
  options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'], threshold: 0.3 },
});

2.5 Layouts

Layout File Used for
base.layout src/layouts/base.layout.vue The shell: sidebar (logo, category menu, favorites, footer with version + commit), top bar (menu/home/search/locale/nav buttons/"Buy me a coffee")
tool.layout src/layouts/tool.layout.vue Wraps a tool page with its title, description, favorite button, and sets <head> meta (title/description/keywords) via @vueuse/head

The active layout is selected per-route via route.meta.layout; the home and About pages use the base layout, tools use the tool layout.

2.6 Internationalization

  • vue-i18n (composition mode, legacy: false) with messages compiled from locales/*.yml and per-tool src/tools/*/locales at build time by @intlify/unplugin-vue-i18n.
  • A translate() helper (src/plugins/i18n.plugin.ts) is used inside tool descriptors; it falls back to the key if a translation is missing.
  • The active locale is persisted to local storage and synced to the i18n runtime in App.vue.

2.7 Build-time plugins (Vite)

The Vite config (vite.config.ts) wires a notable plugin chain that shapes the developer ergonomics:

  • unplugin-auto-import — auto-imports Vue, vue-router, @vueuse/core, vue-i18n, and selected Naive UI composables, so most files don't import { ref, computed }.
  • unplugin-vue-components — auto-registers components from src/, the Naive UI resolver, and icon components (<icon-mdi:...>, <icon-...>).
  • unplugin-icons — on-demand icon components.
  • vite-plugin-vue-markdown — lets .md files be used as components (used by the Git/Regex cheatsheets).
  • vite-svg-loader — import SVGs as Vue components (?component).
  • vite-plugin-pwa (generateSW, registerType: 'autoUpdate') — generates the service worker and web app manifest (name "IT Tools", theme color #18a058).
  • UnoCSS — atomic CSS used throughout templates (flex, mt-12px, op-60, …).

Note

Because of auto-import and auto-component registration, source files use ref, computed, useI18n, <c-button>, <icon-mdi:...>, etc. without explicit imports. This is intentional and is the project's house style — keep it in mind when reading the code.


3. Repository Structure

it-tools/
├─ src/
│  ├─ main.ts                 # App bootstrap: createApp + plugins
│  ├─ App.vue                 # Root component, theme + layout selection
│  ├─ router.ts               # Routes generated from the tool registry
│  ├─ config.ts               # figue config schema (build-time env)
│  ├─ themes.ts               # Naive UI light/dark theme overrides
│  ├─ shims.d.ts              # Module type declarations (.vue, .md, libs)
│  ├─ tools/
│  │  ├─ index.ts             # toolsByCategory / tools / toolsWithCategory
│  │  ├─ tool.ts              # defineTool() factory
│  │  ├─ tools.types.ts       # Tool / ToolCategory types
│  │  ├─ tools.store.ts       # Pinia store (favorites, i18n, categories)
│  │  └─ <tool-name>/         # one folder per tool (~100)
│  │     ├─ index.ts          #   defineTool descriptor
│  │     ├─ <tool>.vue        #   the tool UI
│  │     ├─ <tool>.service.ts #   pure logic (optional)
│  │     ├─ *.test.ts         #   unit tests (optional)
│  │     └─ locales/          #   per-tool translations (optional)
│  ├─ components/             # Shared components (ToolCard, MenuLayout, …)
│  ├─ ui/                     # Custom "c-*" UI kit (c-button, c-card, …) + /c-lib demo
│  ├─ composable/             # Reusable composables (copy, fuzzySearch, validation, …)
│  ├─ modules/                # Feature modules (command-palette, i18n, tracker, shared)
│  ├─ stores/                 # Global Pinia stores (style)
│  ├─ layouts/                # base.layout, tool.layout
│  ├─ pages/                  # Home, About, 404
│  ├─ plugins/                # i18n, naive, plausible Vue plugins
│  ├─ utils/                  # Generic helpers
│  └─ assets/                 # Images, gradients
├─ locales/                   # App-level i18n (en, fr, de, es, pt, uk, vi, zh, no)
├─ scripts/                   # create-tool.mjs, build-locales-files.mjs, release.mjs, …
├─ _templates/                # hygen templates (UI component scaffolding)
├─ Dockerfile                 # Multi-stage build → nginx
├─ nginx.conf                 # SPA server config
├─ vite.config.ts             # Build + plugin configuration
└─ package.json               # Scripts and dependencies

Component layering

  • src/ui/ — a small in-house component kit prefixed c- (c-button, c-card, c-input-text, c-tooltip, c-modal, c-select, c-table, …). These wrap or replace Naive UI primitives for a consistent look. The /c-lib route (development only) is a living style guide for them.
  • src/components/ — app-specific shared components: ToolCard, ColoredCard, CollapsibleToolMenu, MenuLayout, NavbarButtons, FavoriteButton, MenuIconItem, and copyable input/area helpers (InputCopyable, TextareaCopyable, SpanCopyable, FormatTransformer).
  • src/composable/ — logic reuse: useCopy (clipboard + toast), useFuzzySearch (Fuse.js), useValidation (rule-based input validation), useQueryParam/useQueryParamOrStorage (state in URL + storage), useDownloadFileFromBase64, computedRefreshable, useDebouncedRef.

4. Requirements

Runtime (to serve the app)

Requirement Detail
Docker Recommended path. Any host that runs Docker.
Web server (manual) Any static-file server (nginx, Caddy, Apache, S3+CDN) capable of SPA fallback.
Memory Minimal — the production container is nginx serving static files; tens of MB.
Disk Tens of MB for the image/bundle.
TLS Not provided by the app; terminate TLS at a reverse proxy or CDN.

Build (to compile from source)

Requirement Detail
Node.js LTS (the Dockerfile uses node:lts-alpine; @tsconfig/node18 targets Node 18+).
pnpm 9.11.0 (declared via packageManager).
Build memory The build raises Node's heap: NODE_OPTIONS=--max_old_space_size=4096. Give the build host ≥4 GB.

Warning

The production build is memory-hungry — it sets --max_old_space_size=4096. Building on a small VPS (1–2 GB RAM) can OOM-kill the build. Build on a 4 GB+ machine or use the prebuilt image.


5. Configuration Reference

This is the most important section for self-hosters. IT-Tools is configured at build time, not run time.

5.1 The configuration schema

src/config.ts defines the entire configuration surface using figue. Each key declares a type, a default, and the environment variable it reads:

Config key Env variable Type Default Purpose
app.version PACKAGE_VERSION string 0.0.0 App version shown in the sidebar footer.
app.lastCommitSha VITE_VERCEL_GIT_COMMIT_SHA string '' Commit SHA shown in the footer (links to GitHub).
app.baseUrl BASE_URL string / Base path the app is served from.
app.env VITE_VERCEL_ENV enum development One of production, development, preview, test. Gates the dev-only UI lab.
plausible.isTrackerEnabled VITE_TRACKER_ENABLED boolean false Master switch for Plausible analytics.
plausible.domain VITE_PLAUSIBLE_DOMAIN string '' Plausible site domain.
plausible.apiHost VITE_PLAUSIBLE_API_HOST string '' Plausible API host (for self-hosted Plausible).
plausible.trackLocalhost (none) boolean false Whether to track localhost.
showBanner VITE_SHOW_BANNER boolean false Show the "star us on GitHub" banner on home.
showSponsorBanner VITE_SHOW_SPONSOR_BANNER boolean false Show the sponsor banner.

The config is loaded from import.meta.env, validated, and frozen at startup:

export const config = figue({ /* schema above */ })
  .loadEnv({ ...import.meta.env, PACKAGE_VERSION: import.meta.env.PACKAGE_VERSION })
  .validate()
  .getConfig();

5.2 Why these are build-time, not run-time

Vite statically replaces import.meta.env.* references during vite build. The values are compiled into the JavaScript bundle. PACKAGE_VERSION is wired explicitly in vite.config.ts:

define: {
  'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
},

Caution

Setting these variables on the running container (e.g. docker run -e VITE_TRACKER_ENABLED=true …) has no effect — the bundle was already built. To change analytics, the base path, or banners, you must rebuild the image with the variables present at build time, or build the app yourself and serve the resulting dist/.

5.3 Common build-time configurations

Serve under a sub-path (e.g. behind a reverse proxy at /tools/):

# Build with a base path
BASE_URL=/tools/ pnpm build
# then serve dist/ at https://example.com/tools/

Enable privacy-friendly analytics (build a custom image):

# In a custom Dockerfile derived from the project, set before `pnpm build`:
ENV VITE_TRACKER_ENABLED=true
ENV VITE_PLAUSIBLE_DOMAIN=tools.example.com
ENV VITE_PLAUSIBLE_API_HOST=https://plausible.example.com

The Plausible plugin (src/plugins/plausible.plugin.ts) is a no-op unless isTrackerEnabled is true — it injects a fake tracker that does nothing, so the default self-hosted build collects nothing.

5.4 nginx configuration

The bundled server config (nginx.conf) is intentionally minimal:

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;   # SPA fallback
    }
}

The try_files … /index.html line is essential: it lets the client-side router handle deep links. If you serve the app with a different web server, replicate this fallback.


6. Deployment

docker run -d \
  --name it-tools \
  --restart unless-stopped \
  -p 8080:80 \
  corentinth/it-tools:latest
Flag Meaning
-d Detached (background).
--restart unless-stopped Auto-restart on reboot/crash.
-p 8080:80 Publish container port 80 as host 8080.

GitHub Container Registry alternative: ghcr.io/corentinth/it-tools:latest.

6.2 Docker Compose

services:
  it-tools:
    image: corentinth/it-tools:latest
    container_name: it-tools
    restart: unless-stopped
    ports:
      - "8080:80"
docker compose up -d

6.3 Behind a reverse proxy with TLS

The container speaks plain HTTP on port 80. Terminate TLS upstream. Example with Traefik labels:

services:
  it-tools:
    image: corentinth/it-tools:latest
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.ittools.rule=Host(`tools.example.com`)"
      - "traefik.http.routers.ittools.entrypoints=websecure"
      - "traefik.http.routers.ittools.tls.certresolver=le"
      - "traefik.http.services.ittools.loadbalancer.server.port=80"

Tip

Serving at a domain root needs no special config. Serving under a sub-path requires a build-time BASE_URL (see §5.3) so asset URLs and the router base line up.

6.4 Homelab platforms

IT-Tools is packaged for one-click install on Cloudron, Tipi, and Unraid (Community Applications). These wrap the same image.

6.5 Static hosting (S3/CDN/Pages)

Because the output is static, you can build locally and upload dist/ to any static host:

pnpm install
pnpm build          # outputs ./dist
# upload ./dist to S3 / Netlify / Cloudflare Pages / GitHub Pages

Ensure the host rewrites unknown routes to /index.html (SPA fallback). The official project is continuously deployed on Vercel.

6.6 Deployment checklist

  • [ ] Choose a deployment method (Docker / Compose / static host).
  • [ ] If serving under a sub-path, build with BASE_URL=/your-path/.
  • [ ] Put a TLS-terminating reverse proxy or CDN in front for HTTPS.
  • [ ] Confirm SPA fallback works (deep link like /jwt-parser loads on refresh).
  • [ ] Confirm the camera-using tools work only over HTTPS/localhost (browser requirement).
  • [ ] Decide on analytics (default: off). If enabling, rebuild with Plausible env vars.
  • [ ] Set --restart unless-stopped (or your orchestrator's equivalent).

7. Operations

Because there is no backend or database, operations are simple.

Start / stop / status

docker start it-tools
docker stop it-tools
docker ps --filter name=it-tools
$ docker ps --filter name=it-tools
CONTAINER ID   IMAGE                       STATUS         PORTS                  NAMES
a1b2c3d4e5f6   corentinth/it-tools:latest  Up 2 hours     0.0.0.0:8080->80/tcp   it-tools

Logs

docker logs -f it-tools     # nginx access/error logs

Updating

docker pull corentinth/it-tools:latest
docker stop it-tools && docker rm it-tools
docker run -d --name it-tools --restart unless-stopped -p 8080:80 corentinth/it-tools:latest

(With Compose: docker compose pull && docker compose up -d.)

Tip

Pin a specific image tag instead of latest in production so updates are deliberate and reproducible.

Backup

There is nothing server-side to back up — no database, no uploads, no runtime state. All user state (favorites, theme, language) lives in each user's browser. Your "backup" is simply your deployment configuration (Compose file / Dockerfile customizations) kept in version control.

Health checking

A simple HTTP check on the root path is sufficient:

curl -fsS http://localhost:8080/ > /dev/null && echo "OK"

Resource expectations

The runtime is nginx serving static files. Expect very low CPU and memory; a single small container comfortably serves a team or a department. Scaling, if ever needed, is horizontal and stateless (put N identical containers behind a load balancer or CDN).


8. Security Analysis

8.1 Threat surface is small by design

Aspect Assessment
Server-side input handling None. No backend means no server-side injection, deserialization, or SSRF surface.
Secrets at runtime None. The app holds no credentials; there is nothing to leak from the server.
Data exfiltration Tools run in the browser; data-transformation tools transmit nothing.
Analytics Off by default; when on, Plausible is cookieless and collects page views only.
Authentication None (and none needed — it's a public, read-only static app).

8.2 Client-side considerations

  • Tools process untrusted input in the browser. Formatters and parsers (JSON/XML/YAML/SQL, JWT, user-agent, etc.) operate on user-supplied text. The risk is limited to the user's own session. The HTML WYSIWYG editor and Markdown to HTML render user content; the project includes dompurify for sanitization where HTML is produced — preserve that when modifying those tools.
  • Cryptographic tools are convenience-grade. The Encrypt/decrypt tool exposes RC4 and TripleDES alongside AES. RC4 is insecure and TripleDES is legacy. Document this for users (the User Guide does) and prefer AES. Hashing is done with CryptoJS; bcrypt with bcryptjs.
  • Camera recorder requires explicit browser permission and a secure context (HTTPS or localhost). Media stays client-side.

8.3 Hardening recommendations for self-hosters

  • [ ] Serve over HTTPS via a reverse proxy/CDN. Several tools (camera) require a secure context, and HTTPS protects the app delivery itself.
  • [ ] Add security headers at the proxy: Content-Security-Policy, X-Content-Type-Options: nosniff, Referrer-Policy, X-Frame-Options/frame-ancestors. A strict CSP is feasible since the app loads its own bundled assets (loosen only if you enable analytics to an external Plausible host).
  • [ ] Keep analytics off (the default) unless you operate your own Plausible instance.
  • [ ] Pin image versions and rebuild/redeploy to pick up dependency security fixes.
  • [ ] Restrict network exposure if it's an internal tool (bind to the LAN/VPN, not the public internet).

Note

Verify dependency advisories periodically. The app pulls in a broad set of client libraries (e.g. crypto-js, node-forge, monaco-editor, marked, markdown-it, dompurify); rebuilding on a current base keeps these patched. There is a Dependency Dashboard issue tracking this upstream.


9. Building from Source & Developer Workflow

Setup

pnpm install

Develop with hot reload

pnpm dev          # Vite dev server, default http://localhost:5173

In development, the UI lab is available at /c-lib (the c-* component gallery).

Type-check, build, preview

pnpm build        # vue-tsc --noEmit && vite build  → ./dist
pnpm preview      # serve the production build on :5050
pnpm typecheck    # vue-tsc only

Lint

pnpm lint         # eslint over .vue/.ts/.js (antfu config)

npm scripts reference

Script Command Purpose
dev vite Dev server with HMR.
build vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build Type-check + production build.
preview vite preview --port 5050 Serve the built bundle.
test / test:unit vitest --environment jsdom Unit tests.
test:e2e playwright test End-to-end tests.
test:e2e:dev BASE_URL=… NO_WEB_SERVER=true playwright test E2E against a running dev server.
coverage vitest run --coverage Coverage report.
typecheck vue-tsc --noEmit -p tsconfig.vitest.json Type-check only.
lint eslint src --ext … Lint.
script:create:tool node scripts/create-tool.mjs Scaffold a new tool.
script:create:ui hygen generator ui-component Scaffold a UI component.
release node ./scripts/release.mjs Release automation.

10. Extending: Adding a New Tool

Adding a tool is the most common contribution. The repo ships a scaffolder.

10.1 Scaffold

pnpm run script:create:tool my-tool-name

scripts/create-tool.mjs creates src/tools/my-tool-name/ containing:

  • my-tool-name.vue — the UI (starts with a Lorem ipsum placeholder).
  • index.ts — a defineTool({...}) descriptor pre-filled with the title (Title Case), path: '/my-tool-name', keywords from the name, a default icon, and today's createdAt.
  • my-tool-name.service.ts — empty; put pure logic here.
  • my-tool-name.service.test.ts — a Vitest stub.
  • my-tool-name.e2e.spec.ts — a Playwright stub (checks the page title My-tool-name - IT Tools).

It also inserts the import into src/tools/index.ts automatically (near the top of the import list).

10.2 Finish wiring it

The scaffolder adds the import but does not place the tool in a category. Open src/tools/index.ts and add your imported tool to the appropriate category's components: [...] array:

{ name: 'Development', components: [/* ... */, myToolName] },

10.3 Build the tool

  1. Implement the UI in my-tool-name.vue using the c-* UI kit and Naive UI. Remember Vue APIs and many components are auto-imported — you generally don't write import { ref } from 'vue' or import c-button.
  2. Put pure, testable logic in my-tool-name.service.ts and test it in the .test.ts.
  3. Use shared composables where applicable:
    • useCopy() for copy-to-clipboard with a toast.
    • useValidation() for input validation rules.
    • useQueryParamOrStorage() to keep tool state in the URL + local storage (gives you shareable links and remembered settings for free).
    • FormatTransformer.vue / TextareaCopyable.vue for the common "input → transformed output with copy" pattern.
  4. Add translations: a per-tool locales/ folder, or entries under tools.<key> in locales/en.yml (and other languages). Use translate('tools.<key>.title'/'.description') in the descriptor. Tools with static English text skip i18n (e.g. email-normalizer).
  5. Set icon to a Tabler/Material/MDI icon component.
  6. Optionally add redirectFrom to preserve old URLs and keywords to improve search hits.

10.4 The Tool contract

// src/tools/tools.types.ts
export interface Tool {
  name: string;
  path: string;
  description: string;
  keywords: string[];
  component: () => Promise<Component>;   // lazy import — enables code splitting
  icon: Component;
  redirectFrom?: string[];
  isNew: boolean;                        // computed by defineTool
  createdAt?: Date;
}

Important

Always pass component as a lazy import (() => import('./x.vue')). This is what keeps the initial bundle small — each tool is code-split into its own chunk loaded on demand.


11. Internals Deep Dive

11.1 Favorites system

Favorites are stored as an array of tool paths in useStorage('favoriteToolsName', []). The tools store resolves these against the (i18n-projected) tool list, exposes favoriteTools, and supports add/remove/reorder. The home page uses vuedraggable to reorder favorites and persists the new order via updateFavoriteTools(newOrder). The sidebar surfaces favorites as a synthetic category above the real ones.

11.2 The "New" badge

defineTool sets isNew = isAfter(createdAt, subWeeks(now, 2)). The tools store exposes newTools, rendered both as a home-page section and as a small colored dot in the sidebar (MenuIconItem.vue) and a "New" pill on ToolCard.vue.

11.3 Command palette

command-palette.store.ts composes toolsOptions (every tool, tagged category: 'Tools') with static action options. Fuse.js searches across name (weight 2), description, keywords, and category with threshold: 0.3. Results are grouped by category and capped at 5 per group. Actions can be a route (to), an external link (href), or an imperative action() (e.g. Random tool picks _.sample(tools) and navigates; Toggle dark mode flips the style store).

11.4 Copy & toast pattern

useCopy() (src/composable/copy.ts) wraps @vueuse/core's useClipboard (legacy mode for broad support) and optionally raises a Naive UI success message. The *Copyable components (InputCopyable, TextareaCopyable, SpanCopyable) build the ubiquitous "value + copy button" UI; TextareaCopyable additionally renders syntax-highlighted output via a locally-registered highlight.js (sql/json/xml/yaml/ini/markdown languages).

11.5 State-in-URL pattern

useQueryParam/useQueryParamOrStorage (src/composable/queryParams.ts) serialize typed values (number/string/boolean/object) to and from the route query, with an optional local-storage fallback. Tools that adopt this get shareable, bookmarkable links and remembered settings automatically.

11.6 Validation pattern

useValidation() (src/composable/validation.ts) runs an array of { validator, message, getErrorMessage? } rules against a source ref and produces a reactive { isValid, message, status, attrs } suitable for binding to the c-input-text feedback props. isFalsyOrHasThrown treats a thrown exception or a nil/false return as a validation failure — convenient for "parse must succeed" rules.

11.7 Theming

src/themes.ts defines Naive UI lightThemeOverrides/darkThemeOverrides (primary green #1ea54c, menu/layout/card colors). App.vue selects the theme from the style store's isDarkTheme (driven by useDark(), which respects the OS preference and persists the choice).

11.8 Analytics plumbing

plausible.plugin.ts creates either a real Plausible(...) tracker (when config.plausible.isTrackerEnabled) or a no-op fake, then provides it. tracker.services.ts (useTracker) injects it and exposes trackEvent({ eventName }). In the analyzed source the only explicit event is "Support button clicked" (the Buy-me-a-coffee button); page views are auto-tracked via enableAutoPageviews(). With analytics off (default), all of this is inert.


12. Code Quality & Conventions

Area Convention
Language TypeScript everywhere; .vue SFCs with <script setup lang="ts">.
Imports Vue/router/vueuse/i18n + many components are auto-imported — don't add explicit imports for them.
Styling UnoCSS atomic classes in templates; scoped <style lang="less"> for component-specific CSS.
Components Prefer the c-* UI kit; fall back to Naive UI.
Logic separation Pure logic in *.service.ts, UI in *.vue — keeps logic unit-testable.
Lint/format ESLint with @antfu/eslint-config (+ UnoCSS rules); Prettier available.
i18n User-facing strings via vue-i18n; tool titles/descriptions via translate().
Type safety Build gate: vue-tsc --noEmit runs before vite build.

Strengths: the registry pattern makes the codebase highly uniform and easy to extend; tools are isolated; logic/UI separation aids testing; consistent UI components yield a coherent UX.

Watch-outs: heavy reliance on auto-import can confuse newcomers and some IDEs (the README documents the Volar "Take Over Mode" setup); the breadth of per-tool third-party libraries makes the dependency tree large.


13. Testing Strategy

Layer Tool Location / pattern
Unit Vitest (jsdom) *.test.ts / *.service.test.ts colocated with tools and composables.
E2E Playwright *.e2e.spec.ts colocated with tools (excluded from the Vitest run).
Type vue-tsc Enforced in the build and typecheck scripts.

Run them:

pnpm test            # unit (vitest, jsdom)
pnpm coverage        # unit + coverage
pnpm test:e2e        # playwright (spins up a server)
pnpm test:e2e:dev    # playwright against an already-running dev server

The scaffolder seeds every new tool with a unit-test stub and an E2E stub (the latter asserts the page <title> is <Tool> - IT Tools, which the tool.layout sets via @vueuse/head). Coverage is uneven across the ~100 tools; pure service logic is the best-tested layer.

Tip

When adding a tool, put parsing/transformation logic in *.service.ts and cover it with Vitest — it's far cheaper and more reliable than testing through the Vue component.


14. Potential Improvements

Derived from the source and community-reported issues:

  • Configuration is build-time only. Self-hosters cannot toggle analytics/banners/base path without rebuilding. A small runtime-config approach (e.g. a config.json fetched at startup, or env substitution into index.html at container start) would make the official image far more flexible for self-hosters. (High value for admins.)
  • Replace/clearly mark weak crypto. RC4 and TripleDES in the Encrypt/decrypt tool should be labeled as insecure/legacy in the UI (the docs already warn), or removed in favor of AES-GCM.
  • Dependency surface. The Dependency Dashboard issue (#480) tracks keeping the large client-library set current; automated dependency updates + periodic rebuilds reduce client-side CVE exposure.
  • Accessibility & input edge cases. Community issues report problems such as the ASCII art generator failing on some inputs (#1818) and regex-tester edge cases (#1814) — candidates for targeted fixes and more unit coverage.
  • Favorites portability. Favorites/settings are per-browser local storage. An optional export/import (JSON) would let users move their setup between browsers/devices without a backend.
  • Scaffolder finishing step. create-tool.mjs inserts the import but not the category placement; extending it to also place the tool in a category (or prompting for one) would remove a manual step.
  • i18n completeness. Some non-English locales are partial; a coverage report per locale would help contributors prioritize.

15. Quick Reference

Image & ports

# Run (Docker Hub)
docker run -d --name it-tools --restart unless-stopped -p 8080:80 corentinth/it-tools:latest
# Run (GHCR)
docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corentinth/it-tools:latest
  • Container port: 80 (nginx). Common host mapping: 8080.

Build from source

pnpm install
pnpm build            # → ./dist  (needs ~4GB RAM)
pnpm preview          # serve build on :5050

Build-time config (env, baked into the bundle)

Env var Default Effect
BASE_URL / Serve under a sub-path.
VITE_TRACKER_ENABLED false Enable Plausible analytics.
VITE_PLAUSIBLE_DOMAIN '' Plausible site domain.
VITE_PLAUSIBLE_API_HOST '' Plausible API host.
VITE_SHOW_BANNER false Show the GitHub-star banner.
VITE_SHOW_SPONSOR_BANNER false Show the sponsor banner.
VITE_VERCEL_ENV development App env (production hides the UI lab).
VITE_VERCEL_GIT_COMMIT_SHA '' Footer commit link.
PACKAGE_VERSION 0.0.0 Footer version (auto from npm_package_version).

Add a tool

pnpm run script:create:tool my-tool-name
# then add `myToolName` to a category in src/tools/index.ts

Key files

File Role
src/tools/index.ts Tool registry + categories.
src/tools/tool.ts defineTool() factory.
src/router.ts Routes generated from the registry.
src/config.ts Configuration schema (figue, build-time env).
vite.config.ts Build + plugin chain.
Dockerfile / nginx.conf Production image + SPA server.

Facts that prevent mistakes (negative knowledge)

  • There is no backend, no database, no API, no server-side secrets. Don't look for them.
  • Config is build-time; docker run -e VITE_... does not change a prebuilt image.
  • Analytics are off by default in self-hosted builds.
  • The only Vite-detected runtime/build env keys beyond the figue schema are test/CI vars (CI, BASE_URL, NO_WEB_SERVER from playwright.config.ts); there are no other runtime configuration knobs.
  • Tool components are lazy-loaded; the app is code-split per tool.