Repository navigation
Description
TL;DR
For users
- Show a /dev/null commit on your own site, blog, docs or Notion page, as a card that looks like /dev/null: the same graph rail, formatting, code, images, polls and players.
- From a commit's "…" menu, Embed commit lets you pick what the card shows (the commit, the rest of its thread, replies down to three levels or all), its height and theme, then copy HTML, Markdown or a link.
- The card stays live: a commit that is deleted, rejected or hidden disappears from every page that embeds it. It is read-only: starring, voting and replying happen on /dev/null.
- Creating an embed needs an account; reading one doesn't.
- You decide: turn embeds off in Settings → Profile (organization admins in the organization's settings), and existing cards then read
[embedding disabled]. Embeds are on by default. - No tracking: /dev/null doesn't count views or record where a card is shown.
For maintainers
- New public surfaces:
GET /embed/{uuid}(the card, framable, sessionless, always the guest view),GET /oembedand a staticembed.js; one members-only endpoint,GET /commits/{commit}/embed-link, signs the card's URL for the dialog. - Members-only creation without storage: embed URLs carry a relative HMAC signature over the commit and its settings; unsigned or tampered URLs get no card.
- Abuse: per-IP and per-card rate limits, signature checked before any query, settings normalized to a closed set, at most 40 commits per card, no writes; a 30-second invalidated cache is planned.
- Data: one boolean,
allows_embeds, onusersandorganizations, on by default, exported and erased with the account. PreventFramingkeeps every other page out of frames; the card is its only exception.- The card reads without JavaScript: it is rendered on the server like every page, and only the resize script and a few controls need scripts.
- A partial implementation, draft PR feat: let members embed commits on other sites #1522, produced the screenshots in this RFC. It leaves out the cache, the
429fallback, a constant-query tree and shared rail components. - To weigh: the name (embed, or the git-flavored cherry-pick), and the rollout in six pull requests.
Summary
Let members show a public commit on another website as a card that looks and reads like /dev/null: the commit, optionally the rest of its author's thread, and its replies, down to several levels. The card is an iframe served by /dev/null, so it always reflects the commit's current state, and an oEmbed endpoint lets blogging platforms build it from the dialog's signed link.
Motivation
Developers may quote /dev/null threads in blog posts, newsletters and docs with screenshots. A screenshot loses the links, the code, the polls and the conversation, and it survives the commit's deletion. An embed keeps the content live, keeps the author in control, and brings readers back to the thread.
Goals and non-goals
Goals:
- Embed a single commit, a thread, and replies to replies, chosen by whoever creates the embed.
- Render everything a commit can hold, the same way /dev/null does: text formatting, code blocks, emoji, mentions, hashtags, GitHub and commit links, quote blocks, color swatches, images, link cards, embedded players, polls.
- Draw the same graph rail as /dev/null.
- Respect deletion, moderation and the author's choice on every page that embeds the commit.
Non-goals:
- Interacting from the card: starring, voting, replying and translating stay on /dev/null.
- Measuring embeds: no view counts, no referrers, no tracking.
- Embedding anything other than commits (profiles, logs, Remotes).
Product rules
Who can embed what
- Only signed-in members can create an embed, for any approved, public commit. Readers of the page that embeds it don't need an account: the card is public once created.
- Members-only creation is enforced, not just hidden: every embed URL is signed (see Signed URLs), so a guest can't build one by hand.
- Authors can turn embeds off in Settings → Profile. Embeds are allowed by default.
- For a commit posted as an organization, the organization's setting applies. Embeds are allowed by default, and only the organization's admins can change it.
- Turning embeds off also affects embeds that already exist, since the card is served live.
| User | ![]() |
| Organization | ![]() |
Entry point
- Embed commit sits in the commit's "…" menu, right after Send in Remotes.
- It is hidden from guests, and hidden when the commit can't be embedded: not approved, deleted, or its author (or organization) turned embeds off. This includes your own commits when you turned embeds off.
The modal
The /dev/null dialog (floating close button, title, description, outline Cancel and a primary action), widened to fit a preview. Its settings follow the conventions of the Settings pages: sections separated by hairlines, a title and a description, a switch for a yes/no setting, a segmented control for a choice.
| Section | Control | Default | Shown when |
|---|---|---|---|
| The rest of the thread | Switch | On | The commit starts a self-thread |
| Thread (text only) | None: "Starts from the root commit" | The commit is a reply | |
| Replies | Switch | On | The commit has replies |
| Per level | Segmented: 1, 3, 5, 10 | 3 | Replies is on |
| Levels | Segmented: 1, 2, 3, All | 3 | Replies is on |
| Max height | Segmented: 480px, 640px, 800px, None | 640px | Always |
| Theme | Segmented: Light, Dark, Auto (with icons) | Auto | Always |
| Thread & replies | ![]() |
| Replies | ![]() |
A live preview of the card sits next to the settings, and under them on phones. The footer holds three tabs, each showing its code with the CodeBlock component of commits (highlighting, Wrap lines, copy control), highlighted on the server by the same CodeSpans:
- HTML: the iframe, one attribute per line so it reads without scrolling, and the resize script, for any site that accepts HTML. Copy code prints a mono toast
[embed <hash>] copied, in the voice of the[log <hash>] <body>toast after committing. - Markdown: Markdown renderers (GitHub READMEs, docs) leave frames out, so this tab gives the commit's preview image (
og.png) linked to the commit:[](…/commits/<uuid>). Where Markdown accepts HTML, the HTML tab gives the live card. Copy Markdown prints[embed <hash>] Markdown copied. - Link: the commit URL with the settings and their signature as parameters, for sites that support oEmbed. Copy link prints
[embed <hash>] link copied.
| HTML | ![]() |
| Markdown | ![]() |
| Link | ![]() |
Every frame (the preview, the HTML code, oEmbed's) carries color-scheme: normal, like the card: a frame whose color scheme differs from its document's gets an opaque backdrop, which would show around the card's rounded corners on a dark page.
A failed copy prints fatal: could not copy embed code for <hash>, like the existing Copy link action.
Naming
The feature is called embed, in the UI (Embed commit) and in code (/embed, allows_embeds). PRODUCT.md stops the git metaphor at commit and log outside Remotes, and wants labels anyone understands, so the action keeps a plain word. The git voice lives in the copy instead: the [embed <hash>] copied toast.
Alternative: cherry-pick
If the maintainers want the feature itself to carry a git word, cherry-pick is the proposed alternative: picking a commit and bringing it somewhere else is what an embed author does, and every developer knows the word.
| Where | Embed (proposed) | Cherry-pick (alternative) |
|---|---|---|
| Menu entry | Embed commit | Cherry-pick commit |
| Modal title | Embed commit | Cherry-pick commit |
| Toast | [embed <hash>] copied |
[cherry-pick <hash>] copied |
| Setting | Embeds | Cherry-picks |
| Route | /embed/{commit} |
/cherry-pick/{commit} |
| Column | allows_embeds |
allows_cherry_picks |
It comes with costs:
PRODUCT.mdwould need an explicit exception to "the git metaphor stops at those two nouns", next to the one Remotes already has.- Principle 5 ("Wit in the copy, clarity in the action") asks for labels anyone understands. Cherry-pick commit says less than Embed commit to someone who doesn't use git, and doesn't say the result goes on another website.
- In git, a cherry-pick is an independent copy that no longer follows its source. A /dev/null embed follows its source: a deletion, a rejection or an opt-out shows on the third-party page too. The word suggests the opposite of that guarantee.
For these reasons the RFC keeps embed, and lists cherry-pick for the maintainers to weigh.
What the card shows
- A top-level commit: the commit, then the rest of its self-thread when enabled (
1/3,2/3…), then its replies when enabled. When the thread is shown, the replies branch off the solid trunk between1/3and2/3, like on the commit's page. - A reply: always the whole chain from the root commit down to the reply, then the reply's own replies. The card opens at the top.
- Replies: the first N direct replies, in order, at each level, down to the chosen depth. Each truncated level ends with
N more replies on /dev/nullon a hollow node, like Show the last 2. Quotes are listed among the replies with thequotedlabel. - Commits read like on /dev/null: the same header (name, handle, date, then the
1/3thread badge, and the hash on the right), the same body, code blocks and quotes. Dates are short and absolute (Nov 6), since relative times age badly in an article. - The embedded commit is drawn like the focused commit on its page: its body in 17px, its hash in the commit page's mono line,
14:32 · Nov 6, 2026 · commit 9f3c2a1, rather than in its header. - Actions: /dev/null's actions row, read-only. The star with its count and
› 5 repliesboth open the commit on /dev/null; there is no Quote, Translate or Reply. - Polls: while open, the options link to /dev/null to vote, with the time left, and no results; once closed, the final results.
- Text: always the original. No translation and no Translate control.
- Embedded players: same behavior as on /dev/null (click to play, only on the link that gets the card).
- Footer: the /dev/null logo and wordmark, and View on /dev/null →.
- Max height: past it, the thread scrolls inside the card and the footer stays in view.
| A reply, from the root | ![]() |
| A single commit | ![]() |
| A narrow page | ![]() |
Tombstones
Each commit in the card that can't be shown keeps its place, without author or body, so the rest of the thread still reads:
| Tombstone | When |
|---|---|
[deleted] |
The commit was deleted |
[embedding disabled] |
Its author, or its organization, turned embeds off |
[unavailable] |
The commit is pending or rejected |
This applies to the embedded commit itself: ancestors stay above it and replies below it. A tombstone uses the authorless rail of today's [deleted] placeholder: a dashed Rail Gray avatar.
The card is always rendered as a guest sees /dev/null: no session, so no mutes or blocks to apply.
Freshness
An embed never shows a deleted, rejected or opted-out commit for longer than /dev/null itself does.
Design
The card reuses the commit page's components rather than copying them, so the rail stays identical:
- 72px rail (64px under 460px of card width), the 40px avatar as the node, a 2px lane ring offset from the surface.
- A solid trunk down to the dot on the actions row, then a dashed line in the author's lane.
- Rounded dashed elbows in the parent's lane to 28px reply avatars (24px narrow), 38px of indent per level (32px narrow), flattened past three levels (
maxIndentDepth). - Tokens, type and copy from
DESIGN.md; the card follows Light, Dark or the reader's system.
DESIGN.md gains a Commit Embed component section once the design is settled.
Technical design
Overview
sequenceDiagram
autonumber
participant Author as Embed author
participant App as dev-null.sh
participant Site as Third-party page
participant Reader as Reader's browser
Author->>App: "…" → Embed commit, pick settings
App-->>Author: iframe + embed.js, or commit link with settings
Author->>Site: Paste code or link
alt Link on an oEmbed consumer
Site->>App: GET /oembed?url=…
App-->>Site: rich oEmbed JSON with the iframe
end
Reader->>Site: Open the page
Site-->>Reader: Page with the iframe
Reader->>App: GET /embed/{uuid}?…
App-->>Reader: Server-rendered card, framable, no cookies
Reader->>Reader: Card posts its height, embed.js sizes the iframe
Routes
| Method | Path | Purpose |
|---|---|---|
GET |
/embed/{commit} |
The card, framable, whereUuid('commit') |
GET |
/embed.js |
Resize script, static and long-cached |
GET |
/oembed |
oEmbed provider, JSON |
GET |
/commits/{commit}/embed-link |
Members only: the card's signed URL, the commit link that stands for it, and the HTML, Markdown and Link code, for the dialog |
PUT |
/settings/embeds |
The user's setting, UserEmbedsController |
PUT |
/organizations/{organization}/settings/embeds |
The organization's setting, admins only, OrganizationEmbedsController |
The commit page, when opened from a signed embed link (?embed&…&signature=…), gets an oEmbed discovery link in its <head>, built from the raw request URI so the query keeps the order it was signed in:
<link rel="alternate" type="application/json+oembed"
href="https://dev-null.sh/oembed?url=…" title="…">Embed parameters
Shared by /embed/{commit} and the commit URL (Link tab, behind an embed flag):
| Parameter | Values | Default without parameters |
|---|---|---|
thread |
1 |
Off |
replies |
1, 3, 5, 10 |
Off |
depth |
1, 2, 3, all |
3 when replies is set |
theme |
light, dark |
Auto |
max_height |
480, 640, 800, none |
640 |
Unknown or out-of-range values fall back to their default. The commit is addressed by its UUID, never by its short hash.
Signed URLs
Every embed URL, the iframe's and the Link tab's, carries a signature parameter: an HMAC of the card's path and normalized query, keyed with the application key, the way Laravel's relative signed routes work (URL::signedRoute(absolute: false) and signed:relative, without expiry). Leaving the scheme and host out keeps cards working behind a proxy that terminates TLS, or on another domain of the app; the spike found every card answering 403 through a tunnel with absolute signatures.
- Only the modal, behind authentication, produces signatures.
/embedand/oembedanswer403(401for oEmbed, as the spec expects) to a missing or wrong signature, so the card and the oEmbed fall back to a plain link. - A bare commit link pasted into an oEmbed consumer therefore gives no card: only the Link tab's signed link does.
- Nothing is stored and nothing ties an embed to the member who created it: the signature covers the commit and the settings only.
- Since settings are part of what is signed, nobody can derive other cards from an existing embed, which also closes off cache-busting variants.
- Rotating the application key invalidates every existing embed. That is acceptable for a key rotation, and worth noting in the runbook.
The embed page
- Middleware: outside the
webgroup's session and CSRF handling, the way the cached public group inroutes/web.phpalready is, so the response sets no cookie and always renders the guest view. Excluded fromPreventFraming; it sends its own policy instead:Content-Security-Policy: frame-ancestors *and noX-Frame-Options. - Query: builds the tree once from the embedded commit (ancestors to the root, self-thread, N replies per level down to the depth), with eager loading, and marks each node as visible or as one of the three tombstones. Visibility uses the existing guest rules (
CommitStatus::Approved,deleted_at), plus the author's or organization's embed setting. - Bundle: its own client entry point (
resources/js/embed.tsx), which loads only the card's page, none of the app's live updates, analytics or service worker. On the server, the card renders through the app's SSR bundle; a dedicated SSR entry could slim it further. - Rendering: a server-rendered Inertia page with a bare layout (no app shell), reusing
GraphRail, the thread elbows,CommitBody,CommitImages,CommitPoll(read-only),LinkPreviewandLinkEmbed. Read-only props turn off every control that writes. - Abuse: the page is public and anonymous by design, so it is protected by rate limits, a short cache and bounded work. See Abuse and load.
Without JavaScript
Two scripts are involved, and missing either one leaves a readable card:
| Missing | When | What the reader gets |
|---|---|---|
embed.js, on the framing page |
The site strips <script> but keeps frames (many CMSs, sanitized Markdown) |
The card, at the height written in the code: max_height plus the footer (685px by default). A long thread scrolls inside the card; a short one leaves room below it. With None, the frame itself scrolls. |
| The card's own JavaScript, inside the frame | The reader turned JavaScript off, or the platform sandboxes the frame without allow-scripts |
The whole card, rendered on the server like every page: text, highlighted code, images, links, the rail, closed polls, tombstones. |
- The card is part of the SSR bundle, which renders every page (
ssr.tsxresolves./pages/**/*.tsx); a unit test renders it the same way and reads the whole thread in the server's HTML. - Links open outside the frame with
<base target="_blank">, so they work without the card's click handler. - What only JavaScript adds: the code blocks' copy and Wrap lines controls, click-to-play players, and the height message for
embed.js. - Without JavaScript, an explicit Light or Dark holds, since the server sets it; Auto stays light, since only the browser knows the reader's preference. Times read in the server's time zone rather than the reader's.
- A site that strips
<iframe>too gets nothing from the HTML tab: that's what the Markdown tab and oEmbed are for.
Resize protocol
The card posts its content height to its parent whenever it changes (ResizeObserver):
parent.postMessage({ type: 'devnull:embed:resize', id, height }, '*')embed.js listens for that message, accepts it only from the origin it was itself loaded from (https://dev-null.sh), resizes the one frame whose window sent it (event.source), and listens once even when a page includes it several times. With a max height, the card caps itself and scrolls inside, so the iframe never grows past max_height plus the footer. Without embed.js, the iframe keeps the height written in the snippet and still works.
oEmbed
GET /oembed?url={commit URL}&maxwidth=&maxheight=&format=json returns a rich response:
{
"version": "1.0",
"type": "rich",
"provider_name": "/dev/null",
"provider_url": "https://dev-null.sh",
"author_name": "Mira Okafor",
"author_url": "https://dev-null.sh/@mira",
"html": "<iframe src=\"https://dev-null.sh/embed/…\" … style=\"border:0;max-width:100%;color-scheme:normal\"></iframe><script async src=\"https://dev-null.sh/embed.js\"></script>",
"width": 550,
"height": 685,
"cache_age": 3600
}- The embed parameters on
urlcarry over to the iframe; the height ismax_heightplus the 45px footer (640px when there is no cap), and the width followsmaxwidth, up to 550px. 404for a commit that doesn't exist, and401when the link isn't signed or the commit can't be embedded (unapproved, deleted or opted out), as the oEmbed spec expects, so consumers fall back to a plain link.format=xmlreturns501.author_nameis the profile's name, an organization's for its commits (the member who posted is never revealed).- Platforms with an allowlist (WordPress core, for instance) need /dev/null registered with them; the oEmbed providers registry is the first step.
Data
One boolean per profile owner that can turn embeds off:
| Table | Column | Default | Why it's needed | Who sees it | When it's deleted |
|---|---|---|---|---|---|
users |
allows_embeds |
true |
The author's choice is the feature | The user | With the account |
organizations |
allows_embeds |
true |
Same, for commits posted as the org | The org's admins | With the organization |
- Hidden from serialization except to the owner (and the organization's admins).
- Included in
CreateExport; removed with the row byDeleteUser. - Nothing else is stored: no embed records, no referrers, no counters.
Settings
- Settings → Profile gains an Embeds section in the same shape as Pastéis mode: a title, a description, and a switch.
- The organization's settings gain the same section, editable by admins only (
Organization::isAdmin()).
Abuse and load
Every reader of an article loads the card anonymously, so /embed and /oembed are the cheapest way to make /dev/null do work without an account. They must stay cheap, and cost an attacker more than they cost the server.
- Rate limits, in
AppServiceProvidernext to the existing limiters:embeds: per IP and per commit ("{$ip}|{$commit}"), likeactivity-graph, so a page with several embeds still loads, and generous enough for readers behind a shared address. A global per-IP ceiling on top stops one address from walking every commit.oembed: per IP, lower, since consumers fetch it once and cache it.- A throttled card answers
429withRetry-After; the iframe shows a plain fallback with a link to the commit instead of an error page.
- Bounded work:
- The signature is checked first, before any query: an unsigned or forged URL costs one HMAC.
- Parameters are normalized to their closed set of values, and unknown parameters are dropped. A commit therefore has a few dozen possible cards at most, and only signed ones can be requested, so random query strings can't bypass the cache.
- A card holds at most a fixed number of commits (40, to tune). Past it, the remaining levels end with
N more replies on /dev/null, whatever the depth asked for.depth=allnever walks an unbounded thread. - The tree is built in a constant number of queries, with eager loading, whatever its size.
- An unknown or malformed UUID is rejected by the route constraint before any query.
- Short server cache: the rendered tree is cached per commit and normalized parameters, holding public data only, for 30 seconds, the interval at which /dev/null pages pick up changes. It is forgotten as soon as a commit in the thread is deleted, moderated, or its author changes their embed setting, so the card is never staler than /dev/null (decision 20). Misses for unknown commits are cached too, for the same short time.
- No writes: serving a card never writes to the database or the session.
- Static script:
embed.jsis a versioned static asset with a long cache, served without touching PHP. - Edge protection: if Laravel Cloud's edge or a WAF sits in front of the app,
/embedand/oembedare the routes to give a dedicated rule. That is the maintainers' call and out of scope for the implementation.
Security and privacy
- The embed page is the only framable page. Everything else keeps
PreventFraming. - Embed URLs are signed, so only members create cards (see Signed URLs).
- No cookies on the embed page: no session to ride on, no clickjacking of signed-in actions, nothing written from the card.
- The resize message carries a height only;
embed.jschecks its origin before acting on it. - The card shows only what a guest sees. Pending, rejected and opted-out content never leaves /dev/null; an organization's commit never reveals the member who posted it.
- Third-party players load only after the reader presses play, as today.
Screenshots
Captured on the partial implementation in #1522, running locally, and shown in the sections above:
| Screenshot | Files | Section |
|---|---|---|
| The card in a blog post | 08-site-light.png, 08-site-dark.png |
TL;DR |
| Settings → Profile, Embeds | 01-settings-profile-embeds-{light,dark}.png |
Who can embed what |
| Organization settings, Embeds (admins only) | 02-organization-settings-embeds-{light,dark}.png |
Who can embed what |
| The "…" menu with Embed commit | 03-commit-menu-{light,dark}.png |
Entry point |
| The dialog, on a top-level commit and on a reply | 04-modal-thread-{light,dark}.png, 07-modal-reply-{light,dark}.png |
The modal |
| The HTML, Markdown and Link tabs | 05-modal-{html,markdown,link}-tab-{light,dark}.png |
The modal |
| A reply from the root, a single commit, a narrow page | 08-site-{reply,single,narrow}-{light,dark}.png |
What the card shows |
The [unavailable] tombstone |
09-tombstone-unavailable-{light,dark}.png |
Tombstones |
| The card in Notion, through oEmbed | 10-notion-embed.png (light only) |
oEmbed |
| The card with JavaScript off | 11-card-without-javascript-{light,dark}.png |
Without JavaScript |
Not captured: a throttled card, since the spike answers Laravel's plain 429 and the fallback isn't built.
What the spike leaves out, compared to this RFC: the 30-second cache and its invalidation, the 429 fallback, building the tree in a constant number of queries (it queries each node's replies), and sharing the rail components with the commit page (the card draws the same geometry with its own code).
Rollout
Each step is a pull request of its own. Commit titles follow the upstream rule, without a scope (feat: …).
- Embed setting: migration, user and organization settings sections, export, deletion, policies.
- Embed page: route, middleware exclusions, tree query with tombstones, bare SSR page reusing the thread components in read-only mode.
- Resize script:
embed.jsand the card's height messages. - oEmbed: endpoint and discovery link on the commit page.
- Menu entry and modal: Embed commit in the "…" menu, the signing endpoint, the modal with its live preview, the HTML, Markdown and Link tabs.
- Documentation:
KNOWLEDGE_BASE.md(a Commit embeds section),DESIGN.md(the Commit Embed component),PRODUCT.md(embeds among the surfaces).
Decisions
| # | Topic | Decision |
|---|---|---|
| 1 | Delivery | iframe served by /dev/null, plus oEmbed |
| 2 | Interaction | Read-only; interacting happens on /dev/null |
| 3 | Replies | First N per level, N in 1, 3, 5, 10 |
| 4 | Nesting | 1 to 3 levels, or all, flattened past three like /dev/null |
| 5 | Who can embed | Signed-in members, for any public commit, unless the author opted out; anyone can read a card |
| 6 | Rail | Identical to /dev/null, shared components |
| 7 | Dates | Absolute |
| 8 | Open polls | Options link to /dev/null, results only once closed |
| 9 | Entry point | The commit's "…" menu, hidden when the commit can't be embedded |
| 10 | Translations | Original text only |
| 11 | Measurement | None |
| 12 | Height | Capped (640px by default), then scrolls inside the card |
| 13 | Embedding a reply | Always from the root commit, then the reply's own replies |
| 14 | Actions | /dev/null's actions row, read-only: the star and › N replies open the commit on /dev/null |
| 15 | Quotes | Listed among replies with quoted |
| 16 | Unavailable content | [deleted], [embedding disabled], [unavailable] tombstones |
| 17 | Embedded players | Same as /dev/null |
| 18 | Opt-out | Settings → Profile, allowed by default, exported, erased with the account |
| 19 | Organizations | Organization-level setting, allowed by default, admins only |
| 20 | Freshness | Never staler than /dev/null |
| 21 | Modal | /dev/null dialog with Settings conventions and a live preview |
| 22 | Link tab | Settings and signature as URL parameters; a bare link gives no card |
| 23 | Identifier | The commit's UUID |
| 24 | Column | allows_embeds on users and organizations |
| 25 | Setting endpoints | Dedicated controllers, like UserPasteisModeController |
| 26 | Bundle | A client entry point of its own, with the card's page only; the app's SSR bundle on the server |
| 27 | Naming | "Embed" for the action and in code; the git voice stays in the copy; "cherry-pick" proposed as the alternative |
| 28 | Members only | Every embed URL is signed by the modal; unsigned URLs get no card |
| 29 | Abuse | Per-IP and per-commit rate limits, 30s invalidated cache, capped tree, no writes |
| 30 | Code in the dialog | CodeBlock like commits, highlighted on the server; HTML one attribute per line |
| 31 | Markdown | A Markdown tab with the commit's preview image linked to the commit |
| 32 | Signatures | Relative (path and query), so cards work behind proxies and on other domains |
| 33 | Without JavaScript | Server-rendered card, links through <base target="_blank">; scripts only resize and enhance |











Activity