GitHub - gorgonian/hn-dark-bookmarklet: Hacker News dark mode bookmarklet

2 min read Original article ↗

A dark-mode bookmarklet for Hacker News

What it does

  • Dark theme tuned for OLED iPhone screens
  • Survives navigation: tap once on HN, then click stories, comments, and pagination — dark mode stays on without re-tapping

Install on iPhone (bookmarklet)

iOS Safari won't let you save a javascript: URL directly from the address bar, so use the bookmark-then-edit dance:

  1. Copy the contents of dist/hn-dark.bookmarklet.txt.
  2. In Safari, open any page → share icon → Add Bookmark. Save it.
  3. Tap the bookmarks icon → Edit → tap the new bookmark.
  4. Rename to HN Dark. Tap the URL field, clear it, paste the entire contents of hn-dark.bookmarklet.txt. Save.
  5. Move it to the Favorites folder so it appears in the start-page grid when you tap the address bar.

Use it: navigate to news.ycombinator.com, tap the address bar, tap the HN Dark favorite. Browse normally — dark mode persists across HN navigation.

Project layout

src/
  hn-dark.js       # bookmarklet entry; references __HN_DARK_CSS__
  hn-dark.css      # styles, with CSS variables for easy tuning
build.mjs          # esbuild driver
dist/
  hn-dark.bookmarklet.txt   # the javascript: URL (build output)
  hn-dark.min.js            # same payload without the prefix

Build

npm install
npm run build      # writes dist/hn-dark.bookmarklet.txt
npm run watch      # rebuild on src/ changes

The build pipeline:

  1. esbuild minifies src/hn-dark.css to a string.
  2. esbuild minifies src/hn-dark.js, replacing the __HN_DARK_CSS__ identifier with the minified CSS string via define, and wraps the whole thing in an IIFE.
  3. The output is prefixed with javascript: and saved to dist/hn-dark.bookmarklet.txt.

Why one tap from anywhere isn't possible (with a bookmarklet)

The bookmarklet only does anything on news.ycombinator.com — invoked elsewhere, it's a no-op. It can't redirect-then-style: a top-level navigation destroys the bookmarklet's JS context, and HN's frame-ancestors 'self' blocks iframing it from a non-HN parent.

A truly one-tap-from-anywhere solution requires a Safari extension (more work + Apple developer fee $), or a hosted reverse proxy (untrustworthy).

Why the iframe approach works

  • HN sends Content-Security-Policy: ... frame-ancestors 'self', which per spec overrides its X-Frame-Options: DENY. So HN can frame itself.
  • CSP style-src 'unsafe-inline' allows the injected <style> tag.
  • Same-origin parent + iframe gives full DOM access; cookies and fnid form tokens flow normally, so login/voting/commenting all work.