Skip to content

feat: let members embed commits on other sites - #1522

Draft
jubianchi wants to merge 16 commits into
dev-null-sh:productionfrom
jubianchi:feat/commit-embeds
Draft

jubianchi wants to merge 16 commits into
dev-null-sh:productionfrom
jubianchi:feat/commit-embeds

Conversation

@jubianchi

@jubianchi jubianchi commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A first implementation of the commit embeds described in #1521, so the RFC can be discussed on something that runs. It's a draft: the design is open for discussion there, and this PR will follow what's decided.

Changes

  • An Embeds switch in Settings → Profile and in an organization's settings (admins), on by default, in the export.
  • /embed/{uuid}: a signed, framable, sessionless card showing the commit, its thread and replies the way /dev/null draws them (same header, thread badge and actions row, read-only), with tombstones for what can't be shown. It reads without JavaScript.
  • embed.js to size the frame, and an /oembed endpoint for the signed commit link.
  • Embed commit in the "…" menu, with a dialog that previews the card and hands out HTML, Markdown and a link.

Not built yet: the 30-second cache, the 429 fallback, a constant-query tree, and shared rail components.

Validation

Locally: Pest (5919 passed), vitest (1069), node:test, PHPStan, Pint, tsc, lint. Hosted CI not run yet.

Visual evidence

Light and dark screenshots are in #1521, next to the parts they show. The card on a plain page:

A commit embedded in a blog post

The Embed commit dialog

Device verification

Desktop Chrome on macOS, light and dark, plus a 420px-wide page. Not checked on a phone.

Independent review

Not done yet; to be requested before this leaves draft.

Limitations

The items above that aren't built, and the open questions in #1521 (the name, the rollout in smaller PRs).

Adds an allows_embeds preference, on by default, to users and to
organizations. Users change it in Settings → Profile, organization
admins in the organization's settings, each through a dedicated
controller like the pastéis mode. The preference is exported with the
account and hidden from serialization.

🤖 Generated with Claude Code
GET /embed/{commit} renders a card of the commit, optionally with the
rest of its self-thread and its replies down to three levels or all,
as a guest sees it: the route skips the web group, so it never starts a
session or sets a cookie, and PreventFraming leaves it out in favour of
frame-ancestors *. The URL must be signed, so only the embed dialog can
hand one out, and every value outside the allowed ones falls back to
its default.

Commits that can't be shown keep their place as [deleted],
[embedding disabled] or [unavailable] tombstones. A card holds at most
40 commits, and an embeds limiter budgets each card per address with a
global ceiling per address on top.

The card has its own Vite entry, which loads only its page, and draws
the thread's rail with the commit page's geometry.

🤖 Generated with Claude Code
The card posts its height to the page that frames it whenever it
changes. embed.js, a static script, listens for those messages, trusts
only the ones from /dev/null's origin, and resizes the frame that sent
each one. Including it twice listens once.

🤖 Generated with Claude Code
The embed dialog's Link tab will hand out the commit's page flagged
with `embed` and carrying the card's signed query. GET /oembed turns
such a link into a rich response framing the card, with embed.js, so
blogging platforms build the card from the link alone.

Bare and tampered links answer 401, links that aren't one of our
commits 404, commits that can't be embedded 401, and any format but
JSON 501. The commit's page advertises the endpoint only when it is
opened from such a link. The endpoint skips the session and has its
own per-address limit.

🤖 Generated with Claude Code
The arch tests keep queries away from HTTP resources and controllers
away from abort_if. CommitEmbedQuery now returns models, which the
controller shows through ShownStreams::embedNode, and the oEmbed
statuses (501, 404, 401) move to ShowOEmbedRequest's authorization.

🤖 Generated with Claude Code
Members find Embed commit in a commit's "…" menu, after Send in
Remotes, on approved commits whose author allows embeds: the web now
tells each commit whether it is embeddable.

The dialog follows the Settings pages: switches for the rest of the
thread and the replies, segmented controls for replies per level,
levels, max height and theme. Each change asks the new
commit-embed-link endpoint, members only, for the card's signed URL
and the commit link that stands for it; the preview frames the real
card, and the HTML and Link tabs copy the code with a mono
[embed <hash>] copied toast. The card's dates now read in the reader's
own time, like on /dev/null.

🤖 Generated with Claude Code
With the rest of the thread shown, the author's next commit read twice:
on the trunk, as 2/3, and among the replies. The replies now leave it
out, like on the commit's page.

🤖 Generated with Claude Code
The profile page read it with usePage in its own markup, which the
profile sync tests, rendering the page as a plain function, could not
run. UserEmbedsSetting reads it itself, like PasteisModeSetting.

🤖 Generated with Claude Code
Behind a proxy that terminates TLS, or on another domain of the app,
the card's absolute signature no longer matched the URL the app saw,
so every card answered 403. Embeds are now signed and checked on their
path and query only, like signed:relative.

🤖 Generated with Claude Code
The commit-embed-link endpoint now returns the code to paste,
highlighted by the same CodeSpans as commit bodies, so the dialog shows
it with the CodeBlock component, its wrap toggle and copy control. The
iframe puts one attribute per line, so it reads without scrolling.

A Markdown tab joins HTML and Link: Markdown renderers leave frames
out, so it gives the commit's preview image linked to the commit, for
READMEs and docs.

🤖 Generated with Claude Code
On a dark page, a light corner showed around the card: the app's page
background painted the card's document, and a frame whose color scheme
differs from its document's gets an opaque backdrop. The card's page
stays transparent, and every frame (preview, HTML code, oEmbed) keeps
color-scheme: normal like the card.

🤖 Generated with Claude Code
The tests tsconfig rejected setting a height on frames typed as empty
objects, and the linter wants node:test calls marked with void, like
the other node:test files.

🤖 Generated with Claude Code
The card was already part of the SSR bundle, which renders every page;
a test now renders it the way ssr.tsx does and reads the commit, its
replies, tombstones and footer in the server's HTML, so a reader with
JavaScript off, or a sandboxed frame, still gets the whole card.

Links opened outside the frame through a click handler only; a
<base target="_blank"> now does it without JavaScript too.

🤖 Generated with Claude Code
A card asked for in Dark only turned dark through the inline script, so
with JavaScript off it stayed light. The server now sets the dark class
itself for an explicit Dark; only Auto still asks the browser.

🤖 Generated with Claude Code
@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@jubianchi jubianchi mentioned this pull request Nov 8, 2026
The footer keeps the /dev/null wordmark and "View on /dev/null".

🤖 Generated with Claude Code
The header now reads like a commit on /dev/null: name, handle, date and
the 1/3 thread badge, with the hash on the right except on the embedded
commit, which carries it in its commit line like the focused commit on
its page. The embedded commit's body is 17px, as there.

The counts become /dev/null's actions row, read-only: the star with its
count and the "› 5 replies" link, both opening the commit on /dev/null.
Dates stay absolute.

🤖 Generated with Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant