feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(coil): a real link-preview card for shared URLs - #84

Merged
radroid merged 1 commit into
mainfrom
t3x/coil-og-card
Aug 12, 2026
Merged

feat(coil): a real link-preview card for shared URLs#84
radroid merged 1 commit into
mainfrom
t3x/coil-og-card

Conversation

@radroid

Copy link
Copy Markdown
Owner

Sharing coil.curlycloud.dev on WhatsApp or social produced a poor card. Three causes, all in the markup:

BeforeAfter
og:image/icon.png — 1024×1024 square, 1.1 MB/og.png — 1200×630, 74 KB
twitter:cardsummary (small square)summary_large_image (banner)
descriptionpre-repositioning copymatches the current page
dimensionsabsentog:image:width/height/type/alt

A square at 1.1 MB is the worst case for an unfurler: it renders as a small thumbnail rather than a banner, and that size is past the point where several of them quietly give up. The explicit dimensions matter because WhatsApp reads them from the markup to pick a layout before it fetches the image.

The card

Rendered from the new scripts/og-card.html rather than hand-drawn, so it cannot drift from the site — every colour, font and string in it is the same token or copy the page already uses.

No pnpm run og script on purpose: Playwright is not a dependency here, and adding a browser to regenerate a file that changes twice a year would put ~500 lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. Regeneration steps live in the HTML's own comment.

Verified on the deployed site

  • All tags render with absolute URLs (unfurlers do not resolve relative paths)
  • /og.png → 200, PNG image data, 1200 x 630
  • The 404 page inherits the same tags

Correction to what I said earlier about the 403

I previously reported that Cloudflare bot mitigation blocks all datacenter traffic, and therefore that link previews were definitely broken. Re-testing shows that is too strong:

ClientResult
Bare HTTP client from datacenter (GitHub runner curl, plain fetcher)403
Headless-browser service from datacenter200
Residential browser200

So it discriminates on client fingerprint, not on datacenter IPs wholesale. Whether WhatsApp specifically gets through depends on Cloudflare's verified-bot allowlist, which cannot be tested from here — spoofing the user agent does not reproduce it, because Cloudflare validates verified bots by IP ownership. The definitive test is sharing the link. Everything else that could have broken the card is now fixed.

🤖 Generated with Claude Code

Sharing coil.curlycloud.dev produced a poor card, for three reasons that were
all in the markup rather than anything exotic:
- og:image pointed at /icon.png — a 1024x1024, 1.1 MB square. Unfurlers render a
square as a small thumbnail rather than a banner, and that file size is past
the point where several of them quietly give up.
- twitter:card was `summary`, which is the small square layout by definition.
- The description still described the pre-repositioning page ("auto-updates,
push notifications, and a daily upstream sync").
Adds a purpose-built 1200x630 card at ~74 KB, rendered from the new
scripts/og-card.html so it cannot drift from the site — every colour, font and
piece of copy in it is the same token or string the page uses. Also publishes
og:image:width/height/type/alt, which is what WhatsApp reads to decide between
the banner and thumbnail layouts before it fetches anything.
No `pnpm run og` script on purpose: Playwright is not a dependency here, and
adding a browser to regenerate a file that changes twice a year would put ~500
lines into pnpm-lock.yaml, the fork's second-worst rebase-conflict surface. The
regeneration steps are in the HTML's own comment.
Verified on the deployed site: all tags render with absolute URLs, /og.png
serves 200 as a 1200x630 PNG, and the 404 page inherits the same tags.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ebe3f3f-b6b7-47a4-aa3e-afdf49a306e6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e44bf26 into mainAug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/coil-og-card branch August 12, 2026 02:51
radroid added a commit that referenced this pull request Aug 12, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 17, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
radroid added a commit that referenced this pull request Aug 18, 2026
Both landed on main after this branch was cut, and both added files under the directories it
moves. Two were live breaks rather than cosmetics:
- apps/coil-home/src/lib/install.ts imported the install-instructions JSON by relative path
through scripts/t3x/, which no longer exists.
- index.astro derived the upstream version by splitting the manifest on "-t3x". It now splits on
either suffix, because the relay serves whatever the LAST release published and a -t3x manifest
stays current right up until the first coil build. Splitting on only the new one would have
rendered "0.0.33-t3x.26" where "0.0.33" belongs.
The release workflow's inline body was replaced by #78's generated one, so the app-name edit this
branch made there now belongs to install-instructions.json instead, which carries it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@radroid