Skip to content

Commit e44bf26

Browse files
authored
Merge pull request #84 from radroid/t3x/coil-og-card
feat(coil): a real link-preview card for shared URLs
2 parents 69ab183 + a6ba47a commit e44bf26

3 files changed

Lines changed: 188 additions & 3 deletions

File tree

‎apps/coil-home/public/og.png‎

73.9 KB
Loading
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<!doctype html>
2+
<!--
3+
Source for public/og.png, the social link-preview card.
4+
5+
Rendered rather than hand-drawn so it cannot drift from the site: every colour and font below is
6+
copied from the same tokens Layout.astro defines.
7+
8+
There is deliberately no `pnpm run og` script. Playwright is not a dependency of this repo and
9+
adding a browser to the dependency tree to regenerate a file that changes about twice a year is a
10+
bad trade — it would also put ~500 lines into pnpm-lock.yaml, the fork's second-worst
11+
rebase-conflict surface. To regenerate:
12+
13+
1. Open this file in a browser at exactly 1200x630 and wait for the webfonts.
14+
2. Screenshot the viewport to public/og.png.
15+
3. Quantise it — the raw shot is ~148 KB and WhatsApp gets unreliable past a few hundred KB:
16+
ffmpeg -i og.raw.png -vf palettegen=max_colors=192 pal.png
17+
ffmpeg -i og.raw.png -i pal.png -lavfi paletteuse=dither=sierra2_4a og.png
18+
19+
Keep it 1200x630. Layout.astro publishes those exact numbers as og:image:width/height, and a
20+
mismatch makes some unfurlers letterbox or drop the card.
21+
-->
22+
<html lang="en">
23+
<head>
24+
<meta charset="UTF-8" />
25+
<link rel="preconnect" href="https://fonts.googleapis.com" />
26+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
27+
<link
28+
href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=JetBrains+Mono:wght@400;500&display=swap"
29+
rel="stylesheet"
30+
/>
31+
<style>
32+
:root {
33+
--bg: #09090b;
34+
--fg: #fafafa;
35+
--fg-muted: #a1a1aa;
36+
--fg-dim: #71717a;
37+
--border: rgba(255, 255, 255, 0.08);
38+
--accent-h: 150;
39+
--accent: oklch(0.68 0.17 var(--accent-h));
40+
--font-sans: "DM Sans", -apple-system, system-ui, sans-serif;
41+
--font-mono: "JetBrains Mono", ui-monospace, Menlo, monospace;
42+
}
43+
44+
* {
45+
margin: 0;
46+
padding: 0;
47+
box-sizing: border-box;
48+
}
49+
50+
html,
51+
body {
52+
width: 1200px;
53+
height: 630px;
54+
}
55+
56+
body {
57+
position: relative;
58+
overflow: hidden;
59+
background: var(--bg);
60+
color: var(--fg);
61+
font-family: var(--font-sans);
62+
display: flex;
63+
flex-direction: column;
64+
justify-content: center;
65+
padding: 0 88px;
66+
}
67+
68+
/* The hero's grid + radial mask, at card scale. */
69+
.grid {
70+
position: absolute;
71+
inset: 0;
72+
background-image:
73+
linear-gradient(to right, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
74+
linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
75+
background-size: 48px 48px;
76+
mask-image: radial-gradient(ellipse 80% 70% at 30% 45%, black 40%, transparent 100%);
77+
-webkit-mask-image: radial-gradient(
78+
ellipse 80% 70% at 30% 45%,
79+
black 40%,
80+
transparent 100%
81+
);
82+
}
83+
84+
.glow {
85+
position: absolute;
86+
top: -220px;
87+
right: -180px;
88+
width: 720px;
89+
height: 720px;
90+
border-radius: 50%;
91+
background: radial-gradient(circle, oklch(0.68 0.17 150 / 0.16), transparent 62%);
92+
}
93+
94+
.content {
95+
position: relative;
96+
z-index: 1;
97+
max-width: 880px;
98+
}
99+
100+
.eyebrow {
101+
display: inline-flex;
102+
align-items: center;
103+
gap: 10px;
104+
font-family: var(--font-mono);
105+
font-size: 20px;
106+
letter-spacing: 0.04em;
107+
color: var(--fg-dim);
108+
margin-bottom: 34px;
109+
}
110+
111+
.dot {
112+
width: 9px;
113+
height: 9px;
114+
border-radius: 50%;
115+
background: var(--accent);
116+
}
117+
118+
h1 {
119+
font-size: 82px;
120+
line-height: 1.03;
121+
letter-spacing: -0.035em;
122+
font-weight: 600;
123+
}
124+
125+
h1 .mark {
126+
color: var(--accent);
127+
}
128+
129+
p {
130+
margin-top: 30px;
131+
font-size: 28px;
132+
line-height: 1.45;
133+
color: var(--fg-muted);
134+
max-width: 760px;
135+
}
136+
137+
.foot {
138+
position: absolute;
139+
left: 88px;
140+
right: 88px;
141+
bottom: 54px;
142+
z-index: 1;
143+
display: flex;
144+
align-items: center;
145+
justify-content: space-between;
146+
font-family: var(--font-mono);
147+
font-size: 21px;
148+
color: var(--fg-dim);
149+
border-top: 1px solid var(--border);
150+
padding-top: 26px;
151+
}
152+
</style>
153+
</head>
154+
<body>
155+
<div class="grid"></div>
156+
<div class="glow"></div>
157+
158+
<div class="content">
159+
<div class="eyebrow"><span class="dot"></span>a personal fork of T3 Code</div>
160+
<h1><span class="mark">Coil.</span><br />T3 Code, the way<br />I actually run it.</h1>
161+
<p>Everything upstream ships, plus the things I wanted sooner. Rebased daily.</p>
162+
</div>
163+
164+
<div class="foot">
165+
<span>coil.curlycloud.dev</span>
166+
<span>Curly Cloud</span>
167+
</div>
168+
</body>
169+
</html>

‎apps/coil-home/src/layouts/Layout.astro‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ interface Props {
1010
const {
1111
title = "Coil — a T3 Code fork",
1212
description =
13-
"Coil — a personal fork of T3 Code by Curly Cloud, with auto-updates, push notifications, and a daily upstream sync.",
13+
"A personal fork of T3 Code by Curly Cloud. Everything upstream ships, plus the things I wanted sooner — rebased daily.",
1414
pageClass,
1515
} = Astro.props;
1616
1717
const canonical = new URL(Astro.url.pathname, Astro.site).href;
18-
const ogImage = new URL("/icon.png", Astro.site).href;
18+
19+
// `/og.png`, not `/icon.png`. The icon is a 1024x1024, 1.1 MB square: WhatsApp and friends render a
20+
// square as a small thumbnail rather than a banner, and that file size is past the point where
21+
// several unfurlers quietly give up. `/og.png` is a purpose-built 1200x630 card at ~74 KB, rendered
22+
// from `scripts/og-card.html`. Absolute URL is required — unfurlers do not resolve relative paths.
23+
const ogImage = new URL("/og.png", Astro.site).href;
1924
---
2025

2126
<html lang="en">
@@ -31,10 +36,21 @@ const ogImage = new URL("/icon.png", Astro.site).href;
3136
<meta property="og:title" content={title} />
3237
<meta property="og:description" content={description} />
3338
<meta property="og:image" content={ogImage} />
34-
<meta name="twitter:card" content="summary" />
39+
<!--
40+
WhatsApp reads dimensions from the markup rather than fetching the image first, and without
41+
them it often falls back to the small square layout. They must match og.png exactly.
42+
-->
43+
<meta property="og:image:type" content="image/png" />
44+
<meta property="og:image:width" content="1200" />
45+
<meta property="og:image:height" content="630" />
46+
<meta property="og:image:alt" content="Coil — T3 Code, the way I actually run it." />
47+
<meta property="og:locale" content="en_GB" />
48+
<!-- `summary_large_image`, not `summary`: the latter is the small square thumbnail. -->
49+
<meta name="twitter:card" content="summary_large_image" />
3550
<meta name="twitter:title" content={title} />
3651
<meta name="twitter:description" content={description} />
3752
<meta name="twitter:image" content={ogImage} />
53+
<meta name="twitter:image:alt" content="Coil — T3 Code, the way I actually run it." />
3854

3955
<link rel="preconnect" href="https://fonts.googleapis.com" />
4056
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

0 commit comments

Comments
 (0)