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 devOpen http://localhost:4321. Pages reload as you edit.
Command What it doesnpm 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 filesAdding 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 Meaningname
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 }: foroutput.type: 'image';imageis 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
- Add a route in
server/api.mjs. - Add a tool file with
runsOn: 'server'that calls it:const data = await api('your-route', { host: input })(fromsrc/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 startThat 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 Nodedeploy/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).