If CoolSites helps you find something useful, a coffee helps me keep the directory curated and current.
A curated directory of 586 free tools, open source projects, and hidden gems across 30 categories, built for sysadmins, devs, creators, homelabbers, and the endlessly curious.
Pick a category, search, star what you want to keep, and the directory remembers your groups in the browser. Nothing is sent anywhere. There is no account, no backend and no tracking of any kind.
- 586 curated sites across 30 categories, every entry hand-picked and described
- A browse workspace. Bookmarks stay in the fixed rail beside the category and metadata filters, leaving the rest of the screen for search and results.
- Focused collections. Pick a starting point in the rail, scan every member, then move to another collection without leaving the page.
- Search that narrows. Every word you type has to match. Names tolerate typos, longer fields need a real match, and hits are highlighted.
- Filters that stack. Category, tags, open source only, no account needed, staff picks and the products a tool replaces all combine, with live counts on each. Every count is measured against the list as it already stands, so a chip that says 12 gives you 12, and a term that would leave you with nothing is never offered.
- Sorting. A to Z, Z to A, recently added, shuffle, or the default with your bookmarks floated to the top.
- Ten themes. Eight dark (OLED, Catppuccin, Dracula, Nord, Rosé Pine, GitHub Dark, Midnight, Solarized), one light, and a system option that follows the OS.
- Bookmarks with groups. Star a site, sort it into a colour-coded group, drag to reorder. Removing a bookmark or deleting a group can be undone.
- Export and import. Bookmarks download as versioned JSON and restore on any device. Imports are validated entry by entry and report what they skipped.
- Grid and list views, remembered in the URL along with your search, category, sort, tags and filters, so a view can be shared as a link.
- Keyboard first.
/focuses search, arrows move between cards, Enter opens,?lists every shortcut. - Copy as Markdown, so the current filtered list can be pasted into notes or an issue.
- Random pick from whatever is on screen, and a Wayback Machine link on every card.
- Compare up to five sites side by side.
- Feeds. Atom and JSON Feed of recent entries, generated at build time.
- Works offline after the first visit, and says so when it is showing you cached data.
- No third-party requests. Fonts and favicons ship with the site. Nothing loads from a CDN, and nothing calls home.
- No runtime dependencies. Static HTML, CSS, JavaScript and JSON. No framework, no build step to view it.
- Accessible. Body text clears WCAG AA in every theme, with visible focus, live regions, a skip link, and reduced-motion and forced-colors support.
- Self-hostable on GitHub Pages, any static web server, or the local server in this repo.
| Category | Sites | Highlights |
|---|---|---|
| Desktop Software | 50 | DevToys, PowerToys, Files, Everything, ShareX, VLC, Ventoy, HWiNFO |
| Homelab | 38 | Uptime Kuma, Proxmox, Tailscale, Pi-hole, Jellyfin, Immich, Paperless-ngx |
| Dev Tools | 35 | Regex101, Hoppscotch, CodePen, StackBlitz, Zed, Cursor, Playwright |
| AI & ML | 32 | Ollama, Open WebUI, LM Studio, ComfyUI, Hugging Face, Groq, Dify |
| Creative | 30 | Blender, Figma, Photopea, Excalidraw, Penpot, Coolors, fffuel.co |
| Sysadmin | 27 | Grafana, Netdata, Prometheus, Terraform, Wazuh, Cockpit, btop |
| Fun & Culture | 26 | Neal.fun, Radio Garden, Shadertoy, Geoguessr, xkcd, Noclip, Wiby |
| Privacy | 25 | Mullvad, Tor, Signal, ProtonMail, SearXNG, uBlock Origin, Tails |
| Security | 25 | CyberChef, Have I Been Pwned, VirusTotal, OWASP ZAP, Qualys SSL Labs |
| Productivity | 24 | Obsidian, Notion, Linear, Cal.com, Stirling PDF, NocoDB, Typst |
| Education | 21 | freeCodeCamp, Khan Academy, MIT OCW, CS50, LeetCode, Nand2Tetris |
| CLI Tools | 20 | fzf, ripgrep, bat, eza, lazygit, lazydocker, neovim, starship |
| News & Reference | 19 | Hacker News, ArchWiki, Stack Overflow, AlternativeTo, free-for.dev |
| OSINT | 19 | Shodan, Censys, Wayback Machine, SpiderFoot, TinEye, BuiltWith |
| Torrenting | 18 | qBittorrent, Transmission, Deluge, Jackett, Prowlarr, Stremio |
| Gaming | 16 | RetroArch, itch.io, Lichess, Board Game Arena, OpenTTD, RPCS3 |
| AI Creative | 15 | Midjourney, Suno, Runway, ElevenLabs, Remove.bg, Recraft, Pika |
| File Tools | 15 | Cobalt, yt-dlp, FFmpeg, HandBrake, Pandoc, Croc, PDF24 |
| IP & Network | 15 | BGP.Tools, IPinfo.io, ping.pe, Robtex, HackerTarget, ViewDNS |
| Media Downloads | 13 | yt-dlp, Cobalt, JDownloader, gallery-dl, Stremio |
| Networking | 13 | Wireshark, nmap, PuTTY, mRemoteNG, NetBox, Netbird, WinSCP |
| Data & Viz | 12 | Metabase, Apache Superset, Datawrapper, D3.js, Mermaid Live, Kepler.gl |
| Reading | 11 | Calibre, Readarr, Kavita, Libby, Standard Ebooks |
| Arr Stack | 10 | Sonarr, Radarr, Lidarr, Prowlarr, Bazarr, Overseerr |
| Automation | 10 | n8n, Node-RED, Huginn, Apache Airflow, Pipedream, Make, Temporal |
| DNS Tools | 10 | whatsmydns.net, DNSViz, MXToolbox, DNS Leak Test, Zonemaster |
| Music & Audio | 10 | Ardour, LMMS, Vital, Mixxx, MuseScore, Bandlab, Freesound |
| Speed Tests | 10 | Cloudflare, Fast.com, Waveform Bufferbloat, LibreSpeed, M-Lab |
| Containers | 9 | Docker, Podman, k3s, Lens, Helm, ArgoCD, Harbor, Rancher |
| Indexes & Wikis | 8 | ArchWiki, Gentoo Wiki, FMHY, Awesome Privacy |
CoolSites is a static site with no runtime dependencies. It loads its data from companion JSON files, so it needs to be served over HTTP rather than opened from disk.
- Fork this repository
- Go to Settings > Pages
- Set source to
mainbranch, root folder - Your directory is live at
https://yourusername.github.io/CoolSites/
git clone https://github.com/SysAdminDoc/CoolSites.git
cd CoolSites
npm run serveOpen the printed http://127.0.0.1:4173/ address. Opening index.html straight
from disk will not work, because browsers block file:// fetches; the page says
so and tells you to run the server if you try.
Copy the repository contents into the document root. It works with nginx,
Apache, Caddy, or python3 -m http.server. There is no client-side routing, so
don't add a catch-all rewrite to index.html. It would turn every typo into a
page that looks like the homepage but isn't.
Two things are worth excluding from the document root if you can: the .git
directory and anything starting with a dot. The bundled npm run serve already
refuses them.
The content security policy travels in a <meta> tag, so it applies wherever
you host the page, including GitHub Pages.
Three things a meta tag cannot do, so they need a real server. GitHub Pages sends none of them, which is worth knowing rather than assuming: the hosted copy can be framed by anyone and browsers are free to sniff content types.
Content-Security-Policy: <the policy from index.html>; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
The policy is not written out here because it is not a constant. script-src
and style-src each list a SHA-256 for every inline block, and npm run generate
recomputes those as its last step, so copying the string anywhere means copying
something that goes stale the next time a script or a stylesheet changes. Read it out of the <meta> tag in
index.html, which is what npm run serve does. The only thing worth adding on
top is frame-ancestors, which browsers ignore in a meta tag, so the copy hosted
on GitHub Pages genuinely does not have it.
npm run serve sends all four, so local development matches a properly
configured host.
On nginx, remember that add_header does not inherit into a location block
that sets any header of its own, so these have to be repeated in each one rather
than declared once at the server level.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ sites.json │────>│ Filter Engine │────>│ Card Renderer │
│ │ │ │ │ │
│ One JSON entry │ │ Category match │ │ Grid / List │
│ Name, URL, │ │ + Fuzzy search │ │ + View Trans. │
│ Desc, Tags, │ │ + Sort modes │ │ + Lazy favicons │
│ Metadata │ │ + Metadata │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Data and generated feeds live beside index.html:
| File | What it holds |
|---|---|
sites.json |
Every entry: name, URL, description, category, tags, open source and account flags |
categories.json |
Category names, colours and blurbs |
collections.json |
Curated sub-lists rendered by collections.html |
favicons.json |
Icons inlined as data URIs so no icon is fetched at runtime |
stars.json |
Cached GitHub star counts, refreshed locally, never fetched by the browser |
cache-manifest.json |
When each cache above was refreshed, from where, and its hash |
feeds/ |
Atom and JSON Feed of recent entries, generated by npm run generate |
schemas/ |
JSON Schema for the site, category and collection shapes |
No tracking, no cookies, no analytics, and no third-party requests. Bookmarks
live in localStorage and never leave the browser.
npm run serve # local HTTP server on 127.0.0.1:4173
npm run lint # validate the data, feeds, and every version and count string
npm run generate # regenerate feeds and sync versions and counts
npm run build # lint sources, generate, then re-validate
npm test # drive the app in headless Chrome over CDP
npm run package # write dist/ with a minified single-page build
npm run check:links # check every URL in sites.json and write a report
npm run update:stars # refresh stars.json (set GITHUB_TOKEN to avoid the 60/hour cap)
npm run update:favicons # refresh favicons.json and re-encode oversized icons
npm run sync:repo # set the GitHub description and topics from the same datanpm run lint guarantees no count can drift inside the repository. Nothing
covered anything outside it, so the description on GitHub sat at "588 free
tools" while sites.json held 586, and the first place most people read about
this project was the one place the guarantee did not reach. npm run sync:repo
fixes that from the same source the README badges use, and
npm run sync:repo -- --check reports a difference without changing anything.
It needs the gh CLI. It is deliberately not part of npm run build: a build
that reaches out and mutates something on github.com would be a surprising
build.
There is no lockfile because there is nothing to lock. No runtime dependencies,
no dev dependencies, no framework, no build step to view the page. That is a
deliberate choice rather than an oversight, and npm run lint enforces it: the
day a dependency appears in package.json without a package-lock.json beside
it, the build fails.
npm run build writes sitemap.xml. Its lastmod is the date of the freshest
entry in the directory rather than the moment the build ran, because a timestamp
that moves whenever someone regenerates a feed tells a crawler nothing and
teaches it to stop believing the field. robots.txt points at the sitemap and
disallows only work/, which is where the local link-check report lands.
social-card.png is what appears when the site is pasted into Slack, Discord or
Mastodon. Without it every share is a bare blue link, and sharing is how a
directory gets found. node scripts/generate-social-card.js renders it in
headless Chrome from an HTML card with the repository's own fonts inlined, so it
needs no network and no image library. It is generated by hand rather than by the
build, and it deliberately carries no site count: putting one on it would mean
rewriting a 114KB binary on every commit that adds an entry, and git would keep
every version of it forever.
npm run build writes feeds/directory.opml, the whole directory grouped by
category. OPML is what the webring and blogroll world reads, and it is the one
format that lets somebody take this list somewhere else rather than only look at
it here. Kagi's small-web list and the XXIIVV webring both publish one.
No outline carries an xmlUrl. OPML's usual job is carrying feed subscriptions,
so a reader that sees one will try to poll it, and most of these are sites with
no feed at all. Claiming a feed where there is none breaks the reader that
trusts you.
favicons.json ships in full on every visit, so it is the largest single thing a
reader waits for. The generator asks Google for a 32px icon, but Google has no
icon for a long tail of domains, and the fallbacks hand back whatever the site
happens to have: a multi-resolution .ico carrying every size from 16 to 256px,
or an apple-touch-icon at 180. Four of those were 21% of the file on their own,
the largest at 58KB.
npm run update:favicons now decodes anything over 2KB in the headless Chrome
that already runs the tests, redraws it at 32px, and keeps whichever of WebP or
PNG comes out smaller. It only keeps a result that actually shrank, so an icon
that was already the right size is left alone, and SVG is skipped entirely
because rasterising a vector trades away the thing worth keeping. That took the
cache from 827KB to 593KB across 525 domains. Pass --no-reencode to skip it
when Chrome is unavailable and you only need the fetch.
Two ceilings stop it creeping back: 8KB for any one raster icon, and 700KB for the file. Both fail the lint rather than warn.
An entry in favicons.json is one of three things. A data URI means an icon was
found. false means all four sources were tried and there is nothing to find:
putty.org serves no <link> tags at all, retroarch.com has seven and none of
them is an icon. Absent means nobody has looked yet. Without that middle state
every refresh re-attempted the same thirteen domains and spent four requests each
learning what the last run already knew. --retry-missing asks again, for when
one of them starts publishing an icon.
npm run lint is the gate that matters. It validates every data file against
the schemas, rejects duplicate and colliding URLs, checks the generated feeds
match the data, and fails when any version or count string in index.html,
manifest.json, sw.js or this README has drifted from package.json and
sites.json.
stars.json and favicons.json are copies of somebody else's data, and a copy
with no date on it is indistinguishable from a fresh one. cache-manifest.json
records, for each: the command that wrote it, the sources it read, when the run
finished, how many records it holds, how many fetches failed, and a SHA-256 of
the file. The hash is the part that matters. A manifest carrying only a date
would still read "fetched today" after somebody hand-edited the file, so
npm run lint compares the hash and the record count and fails when either has
moved without a refresh.
npm run update:stars also records whether it ran with a GITHUB_TOKEN.
Without one the API allows 60 requests an hour, the run stops at the first 403,
and what lands is a partial refresh wearing today's date. With that flag in the
manifest the two can be told apart.
The page reads the manifest and hides star counts once the cache is more than 90 days old, and the diagnostics panel says why under Data. A star count is exactly the kind of number that looks current whatever its age. A cache with no manifest record is treated the same way: nobody can date it, so nothing repeats it. Icons have no such limit, because an icon does not go wrong the way a count does.
Two fields, and they mean different things. updatedAt is when the entry's own
text last changed. lastReviewedAt is when a person last opened the site and
confirmed the entry still describes it.
Most of the directory carries 2026-06-25 and no lastReviewedAt. That is the
date of one bulk import, not a review, and pretending otherwise is what made the
"recent" sort and the feeds alphabetical order with a timestamp attached. So the
feeds now publish only entries with a real lastReviewedAt, which is a short
list and an honest one.
After you have actually checked an entry:
npm run review -- https://example.com # reviewed today, text unchanged
npm run review -- https://example.com --changed # the entry's text changed too
npm run review -- --list # how much is still unreviewednpm run lint enforces the rest: no future dates, no review dated before the
change it reviewed, a lastReviewedAt on anything not left over from the
import, and a ceiling on how many entries may carry the import date, so nothing
new can be filed under it to dodge the rule.
Add ?debug=1 to the URL and a panel opens with the app version, where the data
came from, how old it is, the service worker state, which caches exist and how
much they hold, and every error since the page loaded. Two recovery actions sit
at the bottom: copy the report, or clear the caches, unregister the worker and
reload.
It reads this browser and shows you what it found. Nothing is sent anywhere, there is no analytics of any kind in this project, and the panel never opens without the parameter. It is most useful on a self-hosted copy that is serving stale data, which is almost always an old cache the worker is still holding.
npm run check:links walks every URL in sites.json and writes a JSON report to
work/link-check.json. The whole directory takes about a minute. By default it
writes nothing back, because whether a dead entry should be repaired or removed
is an editorial call.
Pass --write and it records what it saw: linkStatus and lastCheckedAt on
each entry it actually reached. That's an observation, not a decision, and it's
the difference between a directory that says "checked last Tuesday" and one that
asks you to take its word. Nothing else in the entry moves. lastCheckedAt is
not lastReviewedAt: a check says the URL answered, a review says a person
opened the page and confirmed the entry still describes it. If the result would
fail the lint, the write is rolled back rather than left for whoever pulls next.
Cards show the check date. Anything that didn't answer normally says what happened and is reachable through the Link issues filter, which is a shareable URL like any other filter.
The point of it is telling apart things that look identical from a script:
| Status | Meaning |
|---|---|
ok |
reachable at the URL on file |
moved |
permanently redirected somewhere genuinely different, so the entry is stale |
redirect |
temporary redirect, nothing to do |
blocked |
a bot wall: it refused this client but serves a browser fine |
dead |
404 or 410 for everyone, including a browser |
tls |
certificate or handshake failure |
dns |
the hostname no longer resolves |
timeout |
no answer inside the timeout, twice |
error |
anything else: a dropped connection, a CDN edge failure, a 500, a redirect that goes nowhere |
dead, tls and dns set a non-zero exit code. Everything else is
information.
Only a 404 or a 410 counts as dead. A 500 or a 503 is a broken server, not a removed page, and failing the run on one would tell you to delete a live entry.
Before calling anything dead it asks once more with a browser user agent and
downgrades to blocked if that works, because bot walls answer scripts with
404s and 503s just as readily as with 403s. One of them handed back a 404 to
the checker while serving a real page to Chrome. The retry also checks the
browser landed on the same URL, so a parked domain that redirects every path to
its homepage does not get read as proof of life.
Some hosts refuse every automated request however politely it asks. Set
checkDisabled on the entry to a sentence saying why, and the checker skips it
and prints the reason instead of reporting the same false alarm every run.
--recheck includes them anyway.
Useful flags: --write to record the result, --filter <text> to check one
site, --limit <n> for a quick sample, --concurrency <n> (default 8),
--timeout <ms> (default 15000), --recheck to include skipped entries, and
--out <path> to put the report somewhere else.
npm run check:links -- --filter github.com
npm run check:links -- --write
npm run check:links -- --concurrency 12 --timeout 20000widget.js drops a short list of entries onto another page. One script tag, no
dependencies, and exactly one request, to sites.json beside it. It renders in a
shadow root so nothing leaks either direction.
<script src="https://your-host/CoolSites/widget.js"
data-category="Homelab"
data-limit="8"></script>| Attribute | Meaning |
|---|---|
data-category |
Show only this category, spelled exactly as the directory does. An unknown name says so rather than rendering an empty box. |
data-limit |
How many entries, 1 to 50. Anything else falls back to 8. |
data-target |
CSS selector for where to mount. Defaults to the script tag's parent. |
data-theme |
auto (default), light or dark. |
It follows the reader's system theme unless you pin it, and you can repaint it from the host page:
coolsites-widget {
--coolsites-bg: #fff;
--coolsites-text: #111;
--coolsites-muted: #666;
--coolsites-border: rgba(0, 0, 0, .12);
--coolsites-accent: #2563eb;
--coolsites-radius: 8px;
}The element carries its contract version in data-version. Offline, a timeout
and a failed load each say which one happened.
If your project is listed here, badge.svg is yours to use. It is one static
file, it loads no font and makes no request of its own, and it reads on a light
or a dark page.
<a href="https://sysadmindoc.github.io/CoolSites/"><img
src="https://sysadmindoc.github.io/CoolSites/badge.svg"
alt="listed on CoolSites" width="128" height="20"></a>[](https://sysadmindoc.github.io/CoolSites/)The easiest way is to open a submission issue. Fill in the form and it gets reviewed from there.
To contribute directly, add an entry to sites.json and run npm run build:
{
"name": "Tool Name",
"url": "https://example.com",
"description": "What it does and why it is worth knowing about.",
"category": "Dev Tools",
"openSource": true,
"requiresAuth": false,
"updatedAt": "2026-08-25",
"tags": ["self-hosted", "docker"],
"keywords": ["reverse proxy", "acme"],
"editorsPick": true,
"alternativeTo": ["Some Paid Thing"]
}keywords, editorsPick, alternativeTo and repository are optional;
everything else is required and enforced by schemas/site.schema.json.
category has to be one of the names in categories.json, and npm run build
updates the per-category counts, the feeds, and the counts in this README for
you.
Tags and keywords are different things. tags is a shared vocabulary and
every value has to already exist in tags.json, which only admits a term three
or more entries have in common. That floor is what keeps it a browsing structure
instead of a second description. Everything else an entry is about goes in
keywords: search reads both at the same weight, so nothing becomes harder to
find, but a term only one site uses never becomes a filter that returns one
result. If a keyword grows to three entries, promote it by adding it to
tags.json and moving it. Lint fails if a tag is missing from the vocabulary,
if a vocabulary tag has fallen below the floor, if the same word appears in both
fields, or if two spellings of one term both get in.
repository is where the source lives when that is not the main link. A project
gets one entry, and lint treats url and repository as one address space, so
listing a project twice fails the build.
| Component | Technology |
|---|---|
| Structure | Semantic HTML5 with ARIA |
| Theme menu | Popover plus CSS anchor positioning, with a scripted fallback for browsers that have neither |
| Styling | Vanilla CSS (custom properties, color-mix, content-visibility, forced-colors) |
| Logic | Vanilla JavaScript, no framework |
| Fonts | Outfit and JetBrains Mono, self-hosted as woff2 |
| Icons | Inline SVG, plus favicons inlined as data URIs at build time |
| Hosting | GitHub Pages or any static web server |
| Local tooling | Node.js 22.17 or newer for the lint, build, package, test and server scripts |
Every word you type has to match, so adding words narrows the list. Names tolerate a typo, longer fields need a real match, and if nothing matches literally the search relaxes and says so.
| Type | What it does |
|---|---|
docker compose |
Both words have to match somewhere |
"command line" |
Matches the phrase exactly, in that order |
#dns |
Only entries carrying that tag or keyword, matched exactly, so #dns never means dnssec |
#"open source" |
A tag with a space in it |
-windows |
Leaves out anything mentioning the word |
-#torrent |
Leaves out every entry carrying that tag |
Anything else is read as words. A stray dash, a lone # or a quote you have
not closed yet narrows nothing rather than emptying the page, which matters
because the results update while you type.
| Key | Action |
|---|---|
/ |
Focus search |
Escape |
Clear the search box and close any open dropdown |
Arrow keys |
Move between cards |
Enter |
Open the focused card |
Shift+Enter |
Open the focused card in a new tab |
? |
Show the shortcut list |
Contributions are welcome. To add a site:
- Fork the repository
- Add your entry to
sites.json - Run
npm run build, which validates the entry and updates the counts and feeds - Open a pull request with the generated files included
These are the rules submissions are actually judged against. They're written down because the first two we turned down were judged against reasons invented in the reply, which isn't fair to anyone who spends time on a submission.
Free means free. The site has to be free, or have a free tier that stands on its own. A tier that exists to show you what you're missing doesn't count. The submission we declined offered the last two days of results plus three full reports a week, with everything else behind a $49.99 a year subscription. Compare that against what's already listed: Ollama and Hugging Face are free to use for the thing they're for. That's the bar.
Six months old, and still moving. A project needs six months of public
history before it's eligible. Plenty of good tools launch and vanish inside a
year, and a directory that lists them is a directory of dead links. If a project
hasn't shipped anything or answered an issue in twelve months, it becomes a
candidate for removal. Some tools are simply finished, so this is a prompt to go
look rather than an automatic delete. npm run update:stars records whether each
repository is archived and when it was last pushed, and prints the ones that have
crossed that line. A card says "Archived" when the author has said so themselves,
which is worth more than any threshold guessed from dates.
Say if it's yours. Submitting your own project is fine and several good
entries got here that way. Say so in the submission. What gets declined is the
pattern where the same link lands in thirty directories the same week, because
that's someone buying backlinks, not telling us about a tool. Every outbound link
here carries rel="nofollow ugc", so there's nothing to buy.
If it's yours, bring two that aren't. A self-submission should also propose two entries you have no stake in. It costs you the same twenty minutes it costs whoever reviews it, and it's the difference between contributing to a directory and using one.
No affiliate links, no sponsored entries, no ads. There is no paid placement and there never will be.
What counts as open source. For software, an OSI-approved licence. For a
curated list or a dataset, where the OSI has approved nothing and never will, an
open content licence: CC0, CC BY or CC BY-SA. Source-available but restricted is
not open whichever way it is spelled, so FSL, BUSL and SSPL entries read
openSource: false and carry a note saying which one and why. Writing this down
matters because the two halves are judged by different standards and it would
otherwise look like an inconsistency rather than a rule.
Check removed.json first. Anything taken out of the directory leaves a
record there saying when and why. Deleting a row on its own records nothing you
can read before opening a pull request, and the predictable result is that
somebody proposes the same site again in good faith and gets turned down for a
reason nobody wrote down. Reversing a decision means deleting the record.
Descriptions say what it does and why it's worth knowing about. One or two sentences. Prefer open source when there's a reasonable alternative.
A worked example of a good self-submission: PR #8
disclosed the affiliation, set openSource: false correctly for a
source-available licence rather than claiming otherwise, and ran the lint before
opening.
Email matt_parker@outlook.com, or open an issue if the problem is already public. Expect a reply within a week. There is no bounty and no formal disclosure process; this is one person maintaining a static site.
In scope: anything that gets script running in a reader's browser, anything that
makes the page fetch from a third party, and anything in scripts/ that would
let a contributed entry run code on a maintainer's machine at build time. The
cached favicons are the interesting surface there: they are 522 base64 blobs
fetched from third-party hosts, and the fetcher sniffs magic bytes rather than
trusting the declared content type, because a server once claimed image/x-icon
for a macOS alias file.
Out of scope: anything about the sites the directory links to. They are other people's, and a listing is not an endorsement of their security. Report a dangerous or compromised entry as an ordinary issue and it will be removed.
There is no SECURITY.md because this repository tracks exactly one Markdown
file by design.
MIT License. See LICENSE for details.
Browse the full directory with all 586 sites.