- Dockerfile: Multi-stage build (deps -> build -> runtime) on
node:24.15.0-alpine. Uses corepack for pnpm, builds withpnpm build, runsnode .output/server/index.mjsas non-rootnodeuser. Exposes port 3000. Includes a healthcheck hittinghttp://127.0.0.1:3000/. - docker-compose.yml: Single
webservice, builds locally, taggedbetter-hn:local, maps port3000:3000, setsNITRO_HOST=0.0.0.0andNITRO_PORT=3000, restart policyunless-stopped. - .dockerignore: Excludes
node_modules,.git,.github, build artifacts,.husky,.env, etc.
- .github/workflows/docker.yml: Triggers on push to
master, tagsv*, andworkflow_dispatch. Buildslinux/amd64image and pushes toghcr.io/<owner>/<repo>. Tags:lateston default branch,sha-<short>always, semver on release tags. Uses GHA buildx cache. Permissions:contents: read,packages: write.
The app previously supported only light and dark themes. A third "system" mode was added that follows the OS color scheme preference.
- Three modes:
system(default),light,dark. Toggle cyclessystem -> light -> dark -> system. - Cookie:
bhn.themestores the selected mode (system,light, ordark). - Resolution: When mode is
system, the effective theme resolves to light or dark based onprefers-color-scheme: darkmedia query. - Reactivity: A
matchMedialistener re-applies the theme live when the OS preference changes while in system mode. - SSR: Server cannot know OS preference, so
systemdefaults to light on the server. The inline script corrects it immediately on the client. - HTML attributes:
<html>getsclass="dark"for the resolved effective theme, anddata-theme="system|light|dark"for the selected mode. Icon visibility in the header is driven bydata-theme.
src/lib/theme.ts: AddedTheme.SYSTEM, changed default toTheme.SYSTEM, addedisTheme(),getResolvedTheme(),getNextTheme()helpers.src/client/ui.ts: Rewrote theme switching for three-state cycle, addedprefers-color-schemelistener, setsdata-themeon<html>.src/components/InlineScript.tsx: Inline boot script now understandssystemmode, resolves it via media query, setsdata-themeand.dark.src/components/App.tsx: Addeddata-themeattribute and resolved theme class on<html>.src/components/Header.tsx: AddedSystemIconas third icon in the theme toggle button.src/components/icons/SystemIcon.tsx: New icon component (Heroicons desktop/monitor outline).src/client/styles/header.scss: Icon visibility now useshtml[data-theme="system|light|dark"]selectors instead of.darkclass.src/render.tsx: Cookie theme value validated withisTheme()instead of cast.src/lib/context.ts:themefield is now required (Themeinstead ofTheme | undefined).
- Removed
@vercel/analytics: Deletedsrc/client/analytics.ts, removed its import fromsrc/client/index.ts, removed the Vercel preset fromnitro.config.ts, removed.vercelfrom clean script. - Removed
js-cookieand@types/js-cookie: ReplacedCookies.get/setcalls insrc/client/ui.tswith nativedocument.cookieAPI. - Removed
rimraf: Replacedrimrafin clean script withrm -rf(not needed on non-Windows). - Removed commit hygiene toolchain:
@commitlint/cli,@commitlint/config-conventional,husky,lint-staged,.husky/,commitlint.config.mjs,.lintstagedrc.mjs. - Removed
@pajecawav/prettier-config: Deleted.prettierrc.mjs, now using Prettier defaults. - Removed Deno Deploy workflow: Deleted
.github/workflows/deno-deploy.yml. - Removed GitHub icon: Removed
<GitHubIcon>fromsrc/components/Header.tsx. - Removed
LICENSEfile (MIT, copyrighted to original author; irrelevant for private fork).
- Smooth comment navigation: Added
scroll-behavior: smoothonhtmlso comment anchor links likenext,prev,root, andparentanimate instead of jumping. - Collapse scroll behavior: Updated
src/client/comments.tsso collapsing a thread keeps the parent comment in view withscrollIntoView({ behavior: "smooth", block: "nearest" }). This means collapsing via the thread line scrolls smoothly only when needed, while clicking the inline fold button usually does not move the viewport. - No sticky mobile header: Removed the mobile sticky header behavior from
src/client/styles/header.scssto match original HN more closely. - Comment scroll offset: Added a small
scroll-margin-top: var(--size-2)to both.commentand.infoinsrc/client/styles/comment.scssso navigated-to comments do not land flush against the top of the viewport. - Top-level thread spacing: Added extra vertical space only between top-level comment threads, using a new
topLevelprop insrc/components/Comment.tsx, applied fromsrc/routes/post/[postId].get.tsx, and styled via.topLevelComment + .topLevelComment { margin-top: var(--size-6); }. Nested replies keep the tighter spacing. - Mobile page padding: Increased horizontal body padding on mobile to
var(--size-4)insrc/client/styles/index.cssso pages have more breathing room near the edges.
src/client/comments.ts: Smooth collapse scroll usingscrollIntoViewwithbehavior: "smooth"andblock: "nearest".src/client/styles/comment.scss: Added comment scroll offset, restored tight nested spacing, and added larger spacing only between top-level threads.src/client/styles/header.scss: Removed the mobile sticky header rule.src/client/styles/index.css: Added smooth anchor scrolling and increased mobile horizontal page padding.src/components/Comment.tsx: Added optionaltopLevelprop and top-level comment class.src/routes/post/[postId].get.tsx: Marks root comments as top-level for spacing.
Requested hnrss-style RSS feeds were added under /rss/*, using a mix of Algolia and light HN page scraping.
- Firehose:
/rss/frontpage,/rss/newest - Self-posts:
/rss/ask,/rss/show,/rss/polls - Alternative:
/rss/classic,/rss/best,/rss/active - Comments:
/rss/bestcomments
- Route structure: A new dynamic Nitro route at
src/routes/rss/[feed].get.tsserves all requested feed slugs, andsrc/routes/rss.get.tsxrenders a human-readable/rssindex page. - Feed sources:
frontpage,newest, andpollsuse the Algolia HN search API directly.ask,show,classic,best,active, andbestcommentsfirst scrape IDs from livenews.ycombinator.compages, then hydrate them through Algolia.
- Requested behavior over exact hnrss parity:
- Feed item discussion links point to this Better HN instance (
/post/:id) instead ofnews.ycombinator.com. bestcommentsitem titles useNew comment by <author>.- Default
countis30, capped at100.
- Feed item discussion links point to this Better HN instance (
- Descriptions:
- Story feeds include hnrss-style descriptions by default.
- Link posts show
Article URL,Comments URL,Points, and# Comments. - Self-posts include the HN self-text, then the metadata block.
bestcommentsdescriptions contain the comment HTML.description=0disables item descriptions.
- Link mode:
- Default item
<link>is the article URL when present. link=commentsswitches item<link>to the local Better HN discussion URL.<comments>always points to the local Better HN discussion URL.
- Default item
- Activity params:
- Story feeds support
points,comments,count,link=comments, anddescription=0. bestcommentssupportscountanddescription=0.
- Story feeds support
- Caching: Feed responses are served with
Cache-Control: public, max-age=120, stale-while-revalidate=120, and upstream Algolia/HN fetches are memoized in-process for 120 seconds.
- RSS index page:
/rsslists all implemented feeds with short descriptions. - Per-topic RSS links:
/top,/new,/ask, and/shownow render a visible RSS icon beside the page title. - Autodiscovery: Those same topic pages now emit
<link rel="alternate" type="application/rss+xml">tags for feed readers. - Header entry: The global header now includes an RSS menu linking to the
/rssindex and each implemented feed.
src/lib/rss.ts: Feed definitions, categories, topic-to-feed mapping, and shared route helpers.src/lib/hnrss.ts: Algolia queries, HN page scraping, query param parsing, in-memory caching, and RSS XML generation.src/routes/rss/[feed].get.ts: Dynamic RSS endpoint for all implemented feed slugs.src/routes/rss.get.tsx: HTML index page for/rss.src/components/icons/RssIcon.tsx: New RSS icon component.src/components/Header.tsx: Added global RSS menu in the header.src/components/Meta.tsx: Added optional RSS autodiscovery link tag support.src/render.tsx:renderPage()now accepts optional RSS metadata for the page<head>.src/lib/context.ts: SSR context now carries optional RSS metadata.src/routes/[topicName].get.tsx: Added per-topic RSS link and autodiscovery metadata.src/client/styles/rss.scss: Styles for the/rssindex page.src/client/styles/topic.scss: Styles for the topic-page RSS icon/header row.src/client/styles/header.scss: Styles for the header RSS menu.src/client/styles/index.css: Imports RSS styles and adds shared screen-reader-only helper class.package.json,pnpm-lock.yaml: Addednode-html-parserfor HN page scraping and HTML normalization.
- Ran
pnpm lint:tsc - Ran
pnpm lint:eslint - Ran
pnpm build - Smoke-tested all implemented feeds over HTTP on a local preview server
- Used
agent-browseragainst the built preview to verify:- topic-page RSS icon presence
- topic-page autodiscovery
<link rel="alternate"> /rssindex page contents- live XML rendering for feed URLs in the browser
- Description cleanup: RSS item descriptions no longer include relative age text like
5 minutes ago. Feed readers already show timestamps, so this avoids stale duplicated time metadata inside the item body. - Comment permalink label:
bestcommentsdescriptions now label the Better HN permalink as[comment]instead of[comments], matching the fact that the link targets a single comment. - Docs cleanup: The
/rssindex page no longer documents the legacydescription=0parameter alias. - Compatibility preserved: The feed code still accepts
description=0as an alias for disabling descriptions, to avoid breaking existing RSS consumers. - Implementation cleanup: Refactored
src/lib/hnrss.tsfor clearer query parsing and shared description rendering, and simplifiedsrc/routes/rss.get.tsxby precomputing feed sections for rendering.
src/lib/hnrss.ts: Removed relative age text from RSS descriptions, keptdescription=0as a compatibility alias internally, refactored shared description/link helpers, and changed comment-feed permalink text to[comment].src/routes/rss.get.tsx: Removed the legacydescription=0docs mention and simplified feed section rendering.
- Re-ran
pnpm lint:tscafter the RSS follow-up refinements - Re-ran
pnpm buildafter the RSS cleanup changes
- Replaced the app favicon set with the official Hacker News orange
Yicon. - Updated the public icon assets used by the existing metadata and PWA manifest endpoints.
src/public/favicon.svg: Replaced with the official HNy18.svgsource.src/public/favicon.ico: Replaced with the provided official ICO asset.src/public/apple-touch-icon.png: Regenerated from the official HN SVG at180x180.src/public/android-chrome-192x192.png: Regenerated from the official HN SVG at192x192.src/public/android-chrome-512x512.png: Regenerated from the official HN SVG at512x512.