No description
  • TypeScript 75.9%
  • CSS 15.9%
  • JavaScript 6.7%
  • HTML 1.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-16 15:53:30 +03:00
entrypoints init 2026-10-16 15:53:30 +03:00
public init 2026-10-16 15:53:30 +03:00
scripts init 2026-10-16 15:53:30 +03:00
utils init 2026-10-16 15:53:30 +03:00
.gitignore init 2026-10-16 15:53:30 +03:00
package-lock.json init 2026-10-16 15:53:30 +03:00
package.json init 2026-10-16 15:53:30 +03:00
README.md init 2026-10-16 15:53:30 +03:00
tsconfig.json init 2026-10-16 15:53:30 +03:00
wxt.config.ts init 2026-10-16 15:53:30 +03:00

Reddit Inline Comments

VIBE CODING WARNING This extension was built with Deepseek v4.1 Flash

A Firefox extension (built with WXT) that adds a Comments button to every post in Reddit's feed and expands the thread inline, right under the post. No permalink, no page load, no losing your place in the feed.

┌──────────────────────────────────────────────────────────────┐
│ r/webdev · u/alice · 3h                        Some title... │  ← compact feed row
│ 💬 Comments 12                                               │  ← injected toggle
├──────────────────────────────────────────────────────────────┤
│ Showing 42 comments   [Best][Top][New][Controversial][Old] ↻ ✕│  ← injected panel
│                                                              │
│ ▾ u/alice  OP  ▲ 15 ▼  1h                                  ↗ │
│     Hello world.                                             │
│   ▾ u/bob  MOD  ▲ 3 ▼  2m                                  ↗ │
│       A reply                                                │
│     ↳ 2 more replies              (deep-links to the thread) │
└──────────────────────────────────────────────────────────────┘

▲/▼ vote on a comment without leaving the feed, and ↗ opens that comment's permalink in a new tab.

Requirements

  • Firefox 142+ (desktop)
  • Node 20+ and npm, only for development

Quick start

npm install
npm run dev        # builds and launches Firefox with the extension loaded

Then browse https://www.reddit.com/ and click a Comments chip.

To produce an installable build:

npm run build      # -> .output/firefox-mv2/
npm run zip        # -> .output/reddit-inline-comments-0.1.0-firefox.zip

For a permanent install, load the zip via about:debugging#/runtime/this-firefox → Load Temporary Add-on, or sign it on addons.mozilla.org.

Settings

Click the toolbar icon to pick the default comment sort — the sort a thread starts with. It defaults to Top.

The in-feed sort switcher writes to the same setting, so choosing New in a panel also makes New the default. The popup, an open feed, and any other context stay in step through storage.onChanged, and a new default applies to the threads you open afterwards without a page reload.

How it works

  content script (page)                    background script (extension page)
  ─────────────────────                    ──────────────────────────────────
  1. find <shreddit-post> in the feed
  2. inject a shadow-DOM host
     as a sibling of the post
  3. on click ──── rfe:fetch-thread ────▶  4. GET /comments/{id}.json?raw_json=1
                                           5. normalise the comment tree
  7. render into the shadow root ◀──────   6. reply (cached 5 min, max 3 at once)

Three things are worth calling out.

Fetching happens in the background script. That keeps the page's Content-Security-Policy out of the picture, sends the request with the extension's host permissions (and the user's Reddit cookies), and lets fetches be cached and rate-limited centrally instead of once per tab.

The UI is a sibling of <shreddit-post>, not a child. Reddit renders an absolutely-positioned overlay link (<a slot="full-post-link" class="absolute inset-0">) across the whole post, so anything inside the element would navigate when clicked.

Every panel gets its own shadow root. Reddit's styles cannot leak in and ours cannot leak out, while inherited CSS custom properties still flow through — which is why the panel follows Reddit's light/dark theme for free.

Voting

Reads go through the background script, but writes happen from the content script (entrypoints/reddit.content/vote.ts), because two of the three things a vote needs are only available there:

  • The CSRF token is in a cookie. csrf_token is a double-submit token, so the content script reads it from document.cookie (falling back to the modhash from /api/me.json). A background page cannot see it.
  • URLs must be absolute. A content script's fetch cannot resolve a relative URL — it throws TypeError: /api/vote is not a valid URL and the request never leaves the browser.

Reddit's current client no longer uses the legacy /api/vote endpoint. It posts a GraphQL operation, and that is exactly what we send:

POST https://www.reddit.com/svc/shreddit/graphql
{"operation":"UpdateCommentVoteState",
 "variables":{"input":{"commentId":"t1_ph21u80","voteState":"UP"}},
 "csrf_token":"<csrf_token>"}

voteState is UP, DOWN or NONE — clicking the arrow you already picked clears it. The UI updates optimistically and rolls back with a short notice if Reddit refuses: a GraphQL errors[0].extensions.code, an HTTP 403/429, and an unreadable token each get their own message.

The transport was verified against a real headless Firefox (MV2) rather than assumed. An absolute-URL fetch from a content script to a host the extension has permission for does carry the session cookies, SameSite=Strict ones included, and a JSON Content-Type does not trigger a CORS preflight.

Comment permalinks use Reddit's own shape, /r/{sub}/comments/{postId}/comment/{commentId}/, which is also what the “N more replies” links target.

Reddit's DOM contract

Everything Reddit-specific lives in one file: entrypoints/reddit.content/posts.ts. The shape it expects (captured from a real compact feed):

<shreddit-feed reload-url="...feedViewType=compactView...">
  <article class="w-full m-0" data-post-id="t3_1x72w5i">
    <shreddit-post
      id="t3_1x72w5i"
      view-type="compactView"
      feedindex="0"
      permalink="/r/SteamDeck/comments/1x72w5i/slug/"
      comment-count="10"
      post-title="..."
      subreddit-prefixed-name="r/SteamDeck"
    >
      <a slot="full-post-link" class="absolute inset-0" href="..."></a>
    </shreddit-post>
    <faceplate-partial ...></faceplate-partial>
  </article>
  <hr />
  <shreddit-feed></shreddit-feed
></shreddit-feed>

A post qualifies if it has a t3_… id, is not promoted, is not inside a comment, and is inside a feed (shreddit-feed, an article[data-post-id], or has feedindex). That last rule is what keeps the button off the big hero post on permalink pages. If Reddit reshuffles their markup, that is the one file to edit.

Comment data

Comments come from Reddit's public JSON endpoint — the same payload the thread page hydrates from, no API key or OAuth needed:

https://www.reddit.com/comments/{id}.json?raw_json=1&sort=confidence&limit=100&depth=6

limit and depth cap one request (see DEFAULT_LIMIT / DEFAULT_DEPTH in entrypoints/background.ts). Replies Reddit holds back appear as a “N more replies” link that deep-links to the thread rather than paginating inline. Results are cached for five minutes in the background and again per panel; the ↻ button sends force to skip both, so it always refetches.

Each comment's likes field (true upvoted, false downvoted, null nothing) seeds its vote control, so the arrows reflect how you have already voted.

Sanitising

body_html is attacker-controlled text from the network, so it is re-sanitised before it is inserted (entrypoints/reddit.content/sanitize.ts): tag and attribute allowlists, on*/style stripped, <script>/<iframe>/<svg>/… dropped, javascript: URLs unwrapped to plain text, images restricted to Reddit's own media hosts, and surviving links forced to target="_blank" rel="noopener noreferrer nofollow". Parsing happens inside an inert <template>, so nothing executes or loads during the pass.

Nothing is collected or transmitted anywhere except the comment requests you trigger and the votes you cast (which Reddit attributes to your own signed-in session). The only local state is your default comment sort.

Project layout

wxt.config.ts                     manifest, permissions, Firefox settings
entrypoints/
  background.ts                   fetch + cache + rate limit + messaging
  popup/
    index.html                    the toolbar settings popup
    main.ts                       builds the sort options from shared constants
    popup.css
  reddit.content/
    index.ts                      feed scanning, MutationObserver, lifecycle
    panel.ts                      the injected shadow-DOM UI
    posts.ts                      all Reddit DOM knowledge
    vote.ts                       comment up/down votes (same-origin writes)
    sanitize.ts                   strict HTML allowlist
    format.ts                     numbers, relative times, Reddit URLs
    styles.css                    injected into each shadow root
utils/
  types.ts                        message + data contracts
  settings.ts                     the persisted default sort, shared by popup + feed
  reddit-thread.ts                pure Reddit JSON -> ThreadData translation
scripts/
  generate-icons.mjs              dependency-free PNG icon generator

Limitations

  • Desktop only. Firefox for Android serves a different layout; the selectors here are for the desktop compact feed.
  • old.reddit.com is not supported and is deliberately excluded from matches in entrypoints/reddit.content/index.ts — it uses unrelated markup, so the extension would need a second adapter.
  • Long threads are partly linked out. Reddit truncates responses; the "N more replies" link opens the thread instead of fetching again.
  • Rate limits. Reddit returns 429 if you open posts very quickly; the panel says so and offers a Retry. Being signed in to reddit.com helps.
  • Voting needs a signed-in reddit.com session, and is cast as that account. The request mirrors Reddit's own client (UpdateCommentVoteState against /svc/shreddit/graphql) and the extension-to-Reddit transport is verified against headless Firefox, but the round trip has never been exercised against a live logged-in Reddit from this environment. If Reddit reshapes its client, this is the path most likely to need updating.
  • more-type pagination and non-feed surfaces (e.g. search result cards with a different container) are not special-cased.