Javascript

A site of small online tools for code, data and text. Each tool is one file in src/tools/. Pages are built as static HTML; a small Node server answers…

Toolbench

A site of small online tools for code, data and text. Each tool is one file in src/tools/. Pages are built as static HTML; a small Node server answers the few tools that cannot run in a browser (DNS, IP, SSL certificate and HTTP header lookups, redirect checks, and "what is my IP").

"Toolbench" is a placeholder name. Change it in site.config.mjs.

Requirements

Node.js 22.12 or newer (node -v to check), on your computer and on the server.

Getting started

npm install
npm run dev

Open http://localhost:4321. Pages reload as you edit.

Command What it does npm run dev Pages on :4321 with live reload, API on :3333 (or PORT in .env) npm test Runs every tool once with its sample input (-- --show prints it) npm run build Writes the finished site to dist/ npm start Serves dist/ and the API on :3333 (this is what runs on the VPS) npm run preview Build, then start

How it is organised

site.config.mjs        Site name, address, category order
src/
  tools/               One file per tool. The file name is the page address.
  lib/                 Helpers shared by tools (samples, options, CSV, hashes, dates ...)
                       tool-types.ts describes a tool definition for the editor
  components/          One workbench per kind of tool (TextTool, UnitTool, DateTool ...)
  scripts/             Browser code for the workbenches and the home page search
  pages/               Home page, the tool page template, 404, sitemap
  layouts/Base.astro   Header, footer and <head> for every page
  styles/global.css    All styling. Colours are variables at the top.
public/               Favicon and og-image.png (the share preview image)
scripts/
  dev.mjs              Starts the API and the page server together
  test-tools.mjs       What `npm test` runs
  make-og-image.mjs    Redraws public/og-image.png from the site name and address
server/
  index.mjs            The Node server
  api.mjs              The /api routes
  lib/                 Input checks (hosts.mjs) and the redirect follower
deploy/                Example nginx and systemd files

Adding a tool

Create a file in src/tools/. Its name becomes the address: src/tools/reverse-text.js is served at /reverse-text. Nothing else needs to be registered; the home page, sitemap and "more in this category" lists pick it up.

This is the whole of src/tools/reverse-text.js, a good one to copy:

import { requireInput } from '../lib/text.js';

export default {
  name: 'Reverse Text',
  category: 'Text and encoding',
  description: 'Reverse the order of the characters in a piece of text.',
  keywords: ['backwards', 'mirror'], // extra words the home page search matches

  input: { label: 'Text', sample: 'Return every tool to its hook.' },
  output: { label: 'Reversed', filename: 'reversed.txt' },
  options: [{ id: 'lines', label: 'Reverse each line separately', type: 'checkbox' }],
  actions: [{ id: 'run', label: 'Reverse' }],

  run({ input, options }) {
    requireInput(input);
    const flip = (text) => [...text].reverse().join('');
    return options.lines ? input.split('\n').map(flip).join('\n') : flip(input);
  },
};

After adding a tool, run npm test. It runs every tool with its sample input and reports any that fail. Tools that call the API are skipped, because the test does not start the server.

If a tool takes two inputs (a diff, say), the workbench only has one box: split the input on a line of =====, as src/tools/text-diff.js does.

What a tool file can contain

Field Meaning name Shown as the page heading and in lists. Required. category Groups tools on the home page. Order is set in site.config.mjs. Required. description One sentence under the heading; also the search-engine description. Required. keywords Extra words for the home page search. about Optional longer text shown under the tool. Blank lines separate paragraphs. input { label, sample, placeholder, accept, multiline }. multiline: false gives a one-line field. input: false means the tool needs no input (generators). output { label, filename, type }. type: 'image' shows a picture instead of text. options A list of settings, see below. actions The buttons. The first is the main (yellow) one. Defaults to a single "Convert". live true re-runs the tool as you type. Use it for tools that are instant. runsOn 'server' for tools that call this site's API, 'api' or 'external' for other services. Changes the privacy note under the tool. run The function that does the work. May be async.

Options:

{ id: 'sort', label: 'Sort keys', type: 'checkbox', default: false }
{ id: 'indent', label: 'Indent', type: 'select', default: '2', choices: [{ value: '2', label: '2 spaces' }] }
{ id: 'root', label: 'Root element', type: 'text', default: 'root', placeholder: '' }
{ id: 'count', label: 'How many', type: 'number', default: 5, min: 1, max: 100 }

run receives { input, options, action }: the input text, an object of option values by id, and the id of the button that was pressed. It returns one of:

  • a string: shown as the output
  • { text, status }: output plus a line of your own under it
  • { text, status, ok: false }: the same, shown as a problem (validators use this)
  • { image, filename, alt }: for output.type: 'image'; image is a data URL

To report a mistake in the input, throw new Error('A sentence the visitor can act on.').

Import libraries at the top of the tool file as usual. Each tool is bundled separately, so a large library is only downloaded on the pages that use it. If a library only works in a browser (it touches window or document when loaded), import it inside run instead: const lib = await import('the-library').

Unit converters

Add kind: 'units' and a list of units. No run function is needed.

export default {
  name: 'Area Converter',
  category: 'Unit converters',
  kind: 'units',
  description: 'Convert between square metres, hectares, square feet and acres.',
  from: 'm2', // the units selected when the page opens
  to: 'ft2',
  // factor: how many square metres one of this unit is.
  units: [
    { id: 'm2', name: 'square metres', one: 'square metre', factor: 1 },
    { id: 'ha', name: 'hectares', one: 'hectare', factor: 10000 },
    { id: 'ft2', name: 'square feet', one: 'square foot', factor: 0.09290304 },
    { id: 'ac', name: 'acres', one: 'acre', factor: 4046.8564224 },
  ],
};

For scales that do not start at zero, give toBase and fromBase functions instead of factor. See src/tools/temperature-converter.js.

Other kinds of tool

kind selects the workbench. Tools with no kind are text tools (the default). Each of these has its own page layout and needs no run function; copy the existing file and change the text.

kind Workbench Example units Value and unit pickers (see above) src/tools/length-converter.js currency Amount and currency pickers, live rates src/tools/currency-converter.js date Days between dates, add or subtract src/tools/date-calculator.js age Age from a date of birth src/tools/age-calculator.js business-days Working days, with a holidays list src/tools/business-days-calculator.js timezone Time zone to time zone conversion src/tools/time-zone-converter.js link No tool on the page, only a button to href src/tools/html-validator.js

To add a new kind, create a component in src/components/ and a script in src/scripts/, then add it to the list in src/pages/[slug].astro.

Tools that need the server

  1. Add a route in server/api.mjs.
  2. Add a tool file with runsOn: 'server' that calls it: const data = await api('your-route', { host: input }) (from src/lib/api.js).

See src/tools/mx-lookup.js for a short example. Routes so far: dns, reverse, ip, myip, ssl, redirects and headers.

myip answers with the address the request came from, so it only shows the visitor's address when TRUST_PROXY=1 is set behind nginx or Apache. Without it it shows the proxy's own address.

Anything a visitor types into such a tool makes your server do something on their behalf, so check the input before using it. server/lib/hosts.mjs has the checks used so far: cleanHost() for domain names and isPublicAddress() to refuse loopback and private-network addresses. Tools that open connections (SSL, redirects, headers) must resolve the name first, check the address with isPublicAddress() and connect to that address. All /api routes share a rate limit (30 requests a minute per visitor by default).

Deploying to a VPS

On the server:

git clone <your repository> /var/www/toolbench   # or copy the folder, without node_modules and dist
cd /var/www/toolbench
npm ci
npm run build
cp .env.example .env        # then edit: set TRUST_PROXY=1 behind nginx or Apache
npm start

That serves the whole site on 127.0.0.1:3333. Put nginx or Apache in front of it for the domain name and HTTPS, and keep it running with systemd or pm2:

  • deploy/nginx.conf.example: nginx serves the static files itself and passes only /api/ to Node
  • deploy/toolbench.service.example: a systemd unit that starts the server at boot and restarts it if it stops

Before going live, set name and url in site.config.mjs, run node scripts/make-og-image.mjs to redraw the share image, and rebuild. The address is used in the sitemap, canonical links and the share preview image.

To update the site later: pull the changes, npm ci, npm run build, then restart the service (only needed when files in server/ changed).