tools.jamesking.io
A collection of small, single-purpose tools. Most are standalone, browser-based web apps; some are skills for an LLM or Apple Shortcuts. The landing page groups them into those three sections. Deployed at tools.jamesking.io.
The guiding principles:
- No build step for the tools themselves — pure HTML, CSS and vanilla JavaScript. Any dependencies are loaded from a CDN.
- Local-first — web apps run entirely in the browser (prefer browser-native APIs over libraries); skills run on your own machine.
- Self-contained — each tool lives in its own folder and works when opened directly.
- Consistent — tools share a common look via the design system.
The only build step in the repo is
build.js, which generates the landing page, the styled README pages, and each tool's changelog and the site's RSS feed from git history. Individual tools never need compiling or bundling.
Repository structure
tools/
├── index.html # GENERATED landing page (do not edit by hand)
├── feed.xml # GENERATED RSS feed of new and updated tools
├── changelog-overrides.json # Reworded or hidden commit messages for changelogs
├── build.js # Generates the index, README pages, changelogs and feed
├── README.md # This file (rendered to /readme on the site)
├── netlify.toml # Deploy config — runs `npm run build`
├── design-system/ # Shared tokens + components (see below)
│ ├── tokens.css
│ ├── components.css
│ └── index.html # styleguide gallery
├── netlify/
│ └── edge-functions/ # e.g. rss-proxy.ts (CORS proxy for feeds)
└── <tool-name>/ # One folder per tool
├── index.html # The tool (for hosted-here tools)
├── README.md # Tool docs (first heading + paragraph feed the index)
├── readme.html # GENERATED from README.md
└── changelog.html # GENERATED from the folder's git history
Adding a new tool
Create a folder in the repo root named in
kebab-case(e.g.pdf-merger/).Add
index.html— your tool. Link the shared design system in the<head>(note the../— tools sit one level below the root):<link rel="stylesheet" href="../design-system/tokens.css"> <link rel="stylesheet" href="../design-system/components.css">Then build the UI from the design-system classes (
btn,card,field,badge,dialog, etc.). Keep only genuinely tool-specific layout in a small inline<style>, and reference tokens viahsl(var(--token))so it tracks the theme. Seesyndicate-elsewhere/for a worked example.Wrap the page in a
.container, choosing its width by what the tool is for:container-narrow(768px) for guides and single forms, the default (1100px), orcontainer-wide(1600px) for workspaces with sidebars or side-by-side panes. Don't set a page width in the tool itself. See Page layout.Start the page with the shared header and end it with the shared footer, so every tool links to its generated Readme and Changelog pages:
<div class="container"> <header class="tool-header"> <a class="btn btn-outline btn-sm" href="/">← All tools</a> <span class="tool-header-title">PDF Merger</span> <div class="tool-header-actions"> <a class="btn btn-outline btn-sm" href="readme.html">Readme</a> <a class="btn btn-outline btn-sm" href="changelog.html">Changelog</a> </div> </header> … <footer class="site-footer"> <a href="https://jamesking.io">jamesking.io</a> </footer> </div>Add
README.md— must start with a level-1 heading and a short summary paragraph. These are extracted for the tool's card on the landing page:# PDF Merger Combine multiple PDFs into one, entirely in your browser.The full README is also rendered as a styled page at
<tool>/readme.html.Commit, then rebuild so the landing page, README pages, changelog and feed pick up the new tool:
npm run buildThe changelog and feed come from git history, so a tool gets its changelog page, sparkline and "New tool" feed item only once its folder has been committed.
Externally-hosted tools
If a tool lives elsewhere (another domain, an Apple Shortcut, etc.), it still gets a card — create a folder with just a README.md and add YAML frontmatter:
---
external: true
url: https://fonts.jamesking.io
---
# Font collection
A short description for the card.
The "Open Tool" button will link to url (opening in a new tab) instead of a local index.html.
Skill-based tools
Some jobs can't run in a browser (e.g. downloading from YouTube). These can be packaged as a Claude Code skill that runs locally: put the skill in <tool>/skill/ (a SKILL.md plus self-contained uv scripts) and make index.html a setup guide. Secrets belong in the user's secret store (Keychain or 1Password), never in the repo. See glanceable-podcast/.
Say plainly where a skill runs. Skills also exist for Chat on claude.ai and for the API, where they run in Anthropic's cloud sandbox; a skill that needs the user's own machine (local network, secret store, installed tools) works only in Claude Code, and its setup guide should say so.
Landing-page sections
Each tool appears under one of three headings on the landing page, set with type in its README frontmatter:
type |
Section | For |
|---|---|---|
web (default) |
Web apps | Tools that run entirely in the browser, hosted here or externally (e.g. the fonts collection) |
skill |
Skills | Tools bundled as a skill for an LLM (see below) |
shortcut |
Shortcuts | Apple Shortcuts |
---
type: shortcut
external: true
url: https://www.icloud.com/shortcuts/…
---
Archiving a tool
When a tool is no longer needed, archive it rather than deleting it: add archived (the date) and archiveNote (why, in Markdown) to its README frontmatter.
---
archived: 2026-10-18
archiveNote: Observable now does this itself, so this bookmarklet is no longer needed.
---
The tool moves to an Archive section at the bottom of the landing page, listed by name with an Archived badge rather than as a card. It keeps its URLs, its page headers get an Archived badge, and the note is shown at the top of its Readme and under the archive day in its changelog. Its changelog marks that day Archived, and the feed gets an Archived item (ID tool:<folder>:archived) in place of that day's update. Add the badge and note to the tool's own index.html by hand. If the commit that archives the tool just restates the note, hide it with changelog-overrides.json.
Folders that are not tools
Shared infrastructure is excluded from the landing page via IGNORE_DIRS in build.js (currently design-system and netlify). Add to that set if you introduce more non-tool folders.
Changelogs and the RSS feed
Each tool's changelog, the activity sparkline on its card, and the site's RSS feed (/feed.xml) are all generated by build.js from the repo's git history. Nothing is written by hand. Commit messages are what readers see, so write them as short, plain descriptions of what changed for someone using the tool.
How commits are turned into entries:
A commit belongs to a tool if it changes files in that tool's folder. Generated files (
readme.html),SMOKE-TEST.mdand.claude/don't count, and merge commits are skipped (the commits they merge are counted instead).Renamed folders are followed, so a tool's history carries over when its folder is renamed (e.g.
youtube-glanceable-podcast→glanceable-podcast).Site-wide commits are left out: a commit that touches two or more tools, or the design system, isn't about any one tool. The exception is the commit that adds a tool, which always counts.
Changes are grouped by day. A tool's first day is marked Added; each later day lists that day's commit messages, linking to each commit on GitHub.
Messages can be reworded or hidden without rewriting git history, using
changelog-overrides.jsonat the repo root. Key each entry by the full commit hash. A string replaces the message, andnullleaves the commit out:{ "<full commit hash>": "Tighten the glanceable podcast docs" }A reworded change has no link to its commit, because GitHub still shows the original message there.
What's generated from that:
<tool>/changelog.html— every day of changes, newest first, with a sparkline beside the heading. It's linked from the tool's header and from its card on the landing page.- Sparklines — commits per week over the last 26 weeks, on a log scale so a single commit still shows. Each card's Changelog button includes one. They're measured from when the site was built, so they move on each deploy.
/feed.xml— an RSS 2.0 feed of the latest 50 items: a New tool item for each tool, then an Updated item per tool per day, and an Archived item when a tool is archived. Item IDs are stable (tool:<folder>:new,tool:<folder>:<date>), so readers don't show items as new again after a rebuild. It's linked from the landing page header and advertised with<link rel="alternate">for feed readers.
Development conventions
- Formatting: 2-space indentation.
- Naming:
camelCasefor JS variables/functions,kebab-casefor CSS classes and folder names. - JavaScript: ES6+,
const/letovervar. Vanilla is the default; React is acceptable via CDN/import-maps where a tool genuinely needs it (seeaudiolog-episode-creator). - Error handling: wrap async/IO work in
try/catch. - Browser support: Chrome, Firefox, Safari, Edge (current versions).
These are also recorded in CLAUDE.md for AI-assisted work.
Design system
A shared, no-build CSS layer keeps the tools visually consistent. It lives in design-system/:
| File | Purpose |
|---|---|
tokens.css |
Design tokens as CSS custom properties — colors (HSL channel triplets), spacing, page widths, type, radii, shadows. Light / dark / system theming. |
components.css |
Class-based components: container (narrow / default / wide / full), btn, card, input/field, select, switch, badge, tabs, dialog, toast, table, prose, plus utilities. |
index.html |
Living gallery of every token and component, with a light/dark toggle. Open this as the reference. |
Theming is driven by data-theme on <html>:
data-theme="light"/"dark"— forced.- No attribute (or
"system") — follows the OS viaprefers-color-scheme.
Tools with a theme setting should resolve it in JS and set data-theme to light/dark.
Conventions: don't redefine component styles inside a tool — add only app-specific layout, and prefer a tool-scoped class over overriding a .btn/.card rule. When you add a new shared component, also add it to index.html. Full details in design-system/README.md.
Build & deploy
npm install # one-time: install build/dev dependencies
npm run build # generate index.html, README pages, changelogs and feed.xml
npm start # serve locally (http-server) and open the index
build.js produces, from the project's README files and git history:
index.html— the landing page (cards + links), styled with the design system.readme/index.html— this README, rendered as a styled page (/readmeon the site).<tool>/readme.html— each tool's README as a styled page (linked from its "Readme" button).<tool>/changelog.html— each tool's changes, grouped by day (see Changelogs and the RSS feed).feed.xml— an RSS feed of new and updated tools, from the same git history.design-system/readme.html— the design-system docs, rendered as a styled page.
All of these are generated artifacts and are gitignored (/index.html, /readme/, */readme.html, */changelog.html, /feed.xml) — never edit them by hand; edit the source README.md / build.js and rebuild.
Deployment: Netlify runs npm run build on deploy (see netlify.toml) and publishes the repo root, so the generated pages are always fresh. The changelogs and feed need the full git history: if the deploy checks out a shallow clone, build.js fetches the rest first, and if history still can't be read it leaves out the changelogs, sparklines and feed rather than failing the build. Edge functions in netlify/edge-functions/ are deployed alongside — e.g. rss-proxy.ts, a CORS proxy used by syndicate-elsewhere to fetch RSS feeds from the browser, and feed.ts, which serves a radio-archive-rss podcast feed encoded into its own URL.