This repository has no description
1{{ define "title" }}Tangled · The next-generation social coding platform{{ end }}
2
3{{ define "extrameta" }}
4 <!-- Open Graph Meta Tags -->
5 <meta property="og:title" content="Tangled · The next-generation social coding platform" />
6 <meta property="og:type" content="website" />
7 <meta property="og:url" content="https://tangled.org" />
8 <meta property="og:description" content="The next-generation social coding platform." />
9 <meta property="og:image" content="https://assets.tangled.network/tangled_og.png" />
10 <meta property="og:image:width" content="1200" />
11 <meta property="og:image:height" content="630" />
12
13 <!-- Twitter Card Meta Tags -->
14 <meta name="twitter:card" content="summary_large_image" />
15 <meta name="twitter:title" content="Tangled" />
16 <meta name="twitter:description" content="The next-generation social coding platform." />
17 <meta name="twitter:image" content="https://assets.tangled.network/tangled_og.png" />
18
19 <!-- Additional SEO -->
20 <meta name="description" content="The next-generation social coding platform. Host repositories on your infrastructure with knots, use stacked pull requests, and run CI with spindles." />
21 <link rel="canonical" href="https://tangled.org" />
22
23 <!-- hls.js: play bluesky video embeds -->
24 <script>
25 document.addEventListener('DOMContentLoaded', () => {
26 const videos = document.querySelectorAll('video[data-hls]');
27 if (!videos.length) return;
28
29 const attach = () => {
30 videos.forEach((video) => {
31 const src = video.dataset.hls;
32 if (video.canPlayType('application/vnd.apple.mpegurl')) {
33 // safari can play HLS natively
34 video.src = src;
35 } else if (window.Hls && Hls.isSupported()) {
36 const hls = new Hls();
37 hls.loadSource(src);
38 hls.attachMedia(video);
39 }
40 });
41 };
42
43 // only fetch hls.js when a browser actually needs it
44 const needsHls = Array.from(videos).some(
45 (v) => !v.canPlayType('application/vnd.apple.mpegurl')
46 );
47 if (!needsHls) {
48 attach();
49 return;
50 }
51
52 const script = document.createElement('script');
53 script.src = '/static/hls.min.js';
54 script.onload = attach;
55 document.head.appendChild(script);
56 });
57 </script>
58{{ end }}
59
60
61{{ define "content" }}
62 <div class="flex flex-col gap-20 md:gap-40 my-20 md:my-32">
63 {{ template "timeline/fragments/hero" . }}
64 {{ template "timeline/fragments/preview" . }}
65 {{ template "features1" . }}
66 {{ template "features2" . }}
67 {{ template "recentUpdates" . }}
68 {{ if .ShowNewsletter }}{{ template "newsletter" . }}{{ end }}
69 </div>
70{{ end }}
71
72{{ block "topbarLayout" . }}
73 <header class="max-w-screen-xl mx-auto w-full col-span-full md:col-span-1 md:col-start-2" style="z-index: 20;">
74 {{ if .LoggedInUser }}
75 <div id="upgrade-banner"
76 hx-get="/upgradeBanner"
77 hx-trigger="load"
78 hx-swap="innerHTML">
79 </div>
80 {{ end }}
81 {{ template "layouts/fragments/topbar" . }}
82 </header>
83{{ end }}
84
85{{ block "footerLayout" . }}
86 <footer class="z-10">
87 {{ template "layouts/fragments/footerFull" . }}
88 </footer>
89{{ end }}
90
91{{ block "bodyClasses" . }}
92 bg-transparent bg-gradient-to-b from-white to-slate-100 dark:bg-none dark:bg-gray-900
93{{ end }}
94
95{{ block "mainLayout" . }}
96 <div class="flex-grow relative">
97 <div
98 class="absolute opacity-50 dark:opacity-5 inset-x-0 top-0 bottom-[-50px] md:bottom-[-150px] pointer-events-none bg-slate-200 dark:bg-slate-400 [mask-image:url('https://assets.tangled.network/yarn_ball_mask.svg')] [mask-repeat:no-repeat] [mask-position:right_bottom] [-webkit-mask-image:url('https://assets.tangled.network/yarn_ball_mask.svg')] [-webkit-mask-repeat:no-repeat] [-webkit-mask-position:right_bottom] w-full">
99 </div>
100 <div class="max-w-screen-xl mx-auto flex flex-col gap-4 relative z-10">
101 {{ block "contentLayout" . }}
102 <main>
103 {{ block "content" . }}{{ end }}
104 </main>
105 {{ end }}
106
107 {{ block "contentAfterLayout" . }}
108 <main>
109 {{ block "contentAfter" . }}{{ end }}
110 </main>
111 {{ end }}
112 </div>
113 </div>
114{{ end }}
115
116{{ define "features1" }}
117 {{ $labelStyle := "normal-case cursor-pointer w-auto md:w-full p-4 md:px-6 rounded bg-white dark:bg-gray-800 font-medium text-base md:text-lg opacity-50 border border-gray-200 dark:border-gray-700 relative overflow-hidden" }}
118 {{ $spanStyle := "z-10 items-center justify-between gap-2 w-full" }}
119 {{ $connectorStyle := "w-0.5 h-6 bg-gray-300 dark:bg-gray-600 opacity-0 mx-auto" }}
120 {{ $contentStyle := "hidden bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 grid-cols-1 animate-fadein" }}
121 {{ $progressOverlayStyle := "absolute inset-0 bg-gray-600/10 dark:bg-gray-100/10 w-0 transition-none" }}
122
123 <style>
124 @media (max-width: 768px) {
125 .features-grid:has(#feature-prs:checked) {
126 grid-template-columns: 1fr auto auto;
127 }
128
129 .features-grid:has(#feature-knots:checked) {
130 grid-template-columns: auto 1fr auto;
131 }
132
133 .features-grid:has(#feature-spindles:checked) {
134 grid-template-columns: auto auto 1fr;
135 }
136
137 #feature-prs:checked ~ label[for="feature-prs"] .label-text,
138 #feature-knots:checked ~ label[for="feature-knots"] .label-text,
139 #feature-spindles:checked ~ label[for="feature-spindles"] .label-text {
140 display: inline-flex !important;
141 }
142 #feature-prs:checked ~ label[for="feature-prs"] .icon-only,
143 #feature-knots:checked ~ label[for="feature-knots"] .icon-only,
144 #feature-spindles:checked ~ label[for="feature-spindles"] .icon-only {
145 display: none !important;
146 }
147 }
148 </style>
149
150 <div class="features-grid w-full grid grid-cols-3 gap-x-6 px-2">
151 <input type="radio" id="feature-prs" name="feature" class="peer/prs hidden" checked />
152 <input type="radio" id="feature-knots" name="feature" class="peer/knots hidden" />
153 <input type="radio" id="feature-spindles" name="feature" class="peer/spindles hidden" />
154
155 <label for="feature-prs" class="{{ $labelStyle }} peer-checked/prs:opacity-100 peer-checked/prs:shadow-sm">
156 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">A better way to review {{ i "git-pull-request" "size-5" }}</span>
157 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "git-pull-request" "size-5" }}</span>
158 <div class="{{ $progressOverlayStyle }}" data-progress="prs"></div>
159 </label>
160
161 <label for="feature-knots" class="{{ $labelStyle }} peer-checked/knots:opacity-100 peer-checked/knots:shadow-sm">
162 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">Completely self-hostable {{ i "hard-drive" "size-5" }}</span>
163 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "hard-drive" "size-5" }}</span>
164 <div class="{{ $progressOverlayStyle }}" data-progress="knots"></div>
165 </label>
166
167 <label for="feature-spindles" class="{{ $labelStyle }} peer-checked/spindles:opacity-100 peer-checked/spindles:shadow-sm">
168 <span class="label-text hidden md:inline-flex {{ $spanStyle }}">Quick and easy CI {{ i "layers-2" "size-5" }}</span>
169 <span class="icon-only inline-flex md:hidden {{ $spanStyle }}">{{ i "layers-2" "size-5" }}</span>
170 <div class="{{ $progressOverlayStyle }}" data-progress="spindles"></div>
171 </label>
172
173 <div class="{{ $connectorStyle }} peer-checked/prs:opacity-100"></div>
174 <div class="{{ $connectorStyle }} peer-checked/knots:opacity-100"></div>
175 <div class="{{ $connectorStyle }} peer-checked/spindles:opacity-100"></div>
176
177 {{ $titleStyle := "text-2xl md:text-6xl my-2 md:mb-4 text-black dark:text-white font-medium" }}
178 {{ $textContentStyle := "p-4 md:p-6 md:text-xl" }}
179 {{ $imgContentStyle := "w-full overflow-hidden place-content-center bg-gradient-to-b from-slate-50 to-slate-100 dark:from-gray-800 dark:to-gray-900 border-t border-gray-200 dark:border-gray-700" }}
180 {{ $linkDesktopStyle := "hover:no-underline items-center gap-2 p-3 text-base btn" }}
181
182 <div class="col-span-3 {{ $contentStyle }} peer-checked/prs:grid">
183 <div class="{{ $textContentStyle }} flex gap-4 md:gap-8">
184 <section class="space-y-4">
185 <h1 class="{{ $titleStyle }}">Pull requests, reimagined</h1>
186 <p class="text-gray-600 dark:text-gray-400">
187 Break down large features into small, reviewable chunks. Stack pull
188 requests on top of each other and ship faster with round-based code
189 reviews. Tangled natively supports stacking using Jujutsu.
190 </p>
191 <a href="https://blog.tangled.org/stacking" class="hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium">
192 Learn more
193 {{ i "arrow-right" "size-4" }}
194 </a>
195 </section>
196 <div class="w-fit hidden md:flex items-center">
197 <div class="size-32 rounded-full flex items-center justify-center bg-yellow-100 dark:bg-yellow-900 -mr-4" >
198 {{ i "wand-sparkles" "size-16 text-yellow-500 dark:text-yellow-500" }}
199 </div>
200 <div class="size-40 rounded-full flex items-center justify-center bg-green-100 dark:bg-green-900 z-10">
201 {{ i "git-pull-request-arrow" "size-24 text-green-500 dark:text-green-500 rotate-12" }}
202 </div>
203 </div>
204 </div>
205 <div class="{{ $imgContentStyle }} relative overflow-hidden flex items-center justify-center">
206 <div class="w-[120%] md:w-full flex">
207 <picture class="w-full">
208 <source srcset="https://assets.tangled.network/home-page-prs-dark.svg" media="(prefers-color-scheme: dark)" />
209 <img src="https://assets.tangled.network/home-page-prs-light.svg" class="w-full block" alt="A list of stacked pull requests with CI checks and PR comments"/>
210 </picture>
211 </div>
212 </div>
213 </div>
214
215 <div class="col-span-3 {{ $contentStyle }} peer-checked/knots:grid">
216 <div class="{{ $textContentStyle }} flex place-content-between gap-4 md:gap-8">
217 <section class="space-y-4">
218 <h1 class="{{ $titleStyle }}">Run it at home</h1>
219 <p class="text-gray-600 dark:text-gray-400">
220 Host your repositories on your own infrastructure with <a href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide" class="no-underline">knots</a>.
221 Run CI on your own machines with <a href="https://docs.tangled.org/spindles.html#self-hosting-guide" class="no-underline">spindles</a>.
222 <br>
223 Don't want to self-host? All are welcome on our hosted instances.
224 </p>
225 <a href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide" class="hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium">
226 Learn more
227 {{ i "arrow-right" "size-4" }}
228 </a>
229 </section>
230 <div class="w-fit hidden md:flex items-center">
231 <div class="size-32 rounded-full flex items-center justify-center bg-blue-100 dark:bg-blue-900 -mr-4" >
232 {{ i "workflow" "size-16 text-blue-500 dark:text-blue-500" }}
233 </div>
234 <div class="size-40 rounded-full flex items-center justify-center bg-yellow-100 dark:bg-yellow-900 z-10">
235 {{ i "server" "size-24 text-yellow-500 dark:text-yellow-500" }}
236 </div>
237 </div>
238 </div>
239 <div class="{{ $imgContentStyle }} relative overflow-hidden flex items-center justify-center">
240 <div class="w-[120%] md:w-full flex">
241 <picture class="w-full">
242 <source srcset="https://assets.tangled.network/home-page-self-host-dark.svg" media="(prefers-color-scheme: dark)" />
243 <img src="https://assets.tangled.network/home-page-self-host-light.svg" class="w-full block" alt="A world map with knots hosting code on servers in different locations across the world" />
244 </picture>
245 </div>
246 </div>
247 </div>
248
249 <div class="col-span-3 {{ $contentStyle }} peer-checked/spindles:grid">
250 <div class="{{ $textContentStyle }} flex place-content-between gap-4 md:gap-8">
251 <section class="space-y-4">
252 <h1 class="{{ $titleStyle }}">Nix-powered CI</h1>
253 <p class="text-gray-600 dark:text-gray-400">
254 Pick and choose dependencies for your CI pipelines from <a
255 href="https://docs.tangled.org/spindles.html#dependencies"
256 class="no-underline"><code>nixpkgs</code></a>, one of the biggest
257 package repositories.
258 <br>
259 Support for Docker and MicroVM based runners coming soon!
260 </p>
261 <a href="https://docs.tangled.org/spindles.html#pipelines" class="hover:no-underline inline-flex items-center gap-2 text-sm md:text-base text-black dark:text-white font-medium">
262 Learn more
263 {{ i "arrow-right" "size-4" }}
264 </a>
265 </section>
266 <div class="w-fit hidden md:flex items-center">
267 <div class="size-32 rounded-full flex items-center justify-center bg-green-100 dark:bg-green-900 -mr-4" >
268 {{ i "package" "size-16 text-green-500 dark:text-green-500" }}
269 </div>
270 <div class="size-40 rounded-full flex items-center justify-center bg-indigo-100 dark:bg-indigo-900 z-10">
271 {{ i "snowflake" "size-24 text-indigo-500 dark:text-indigo-500 rotate-12" }}
272 </div>
273 </div>
274 </div>
275 <div class="{{ $imgContentStyle }} flex items-end justify-center pb-0 overflow-hidden">
276 <div class="w-[120%] md:w-full flex">
277 <picture class="w-full">
278 <source srcset="https://assets.tangled.network/home-page-ci-dark.svg" media="(prefers-color-scheme: dark)" />
279 <img src="https://assets.tangled.network/home-page-ci-light.svg" class="w-full block" alt="The Tangled UI showing status checks, a Nix-based CI workflow YAML file, and logs from a CI job"/>
280 </picture>
281 </div>
282 </div>
283 </div>
284 </div>
285
286 <script>
287 document.addEventListener('DOMContentLoaded', function() {
288 const featureIds = ['feature-prs', 'feature-knots', 'feature-spindles'];
289 const progressNames = ['prs', 'knots', 'spindles'];
290 let currentIndex = 0;
291 let timerInterval = null;
292
293 function stopTimer() {
294 if (timerInterval) {
295 clearInterval(timerInterval);
296 timerInterval = null;
297 }
298 document.querySelectorAll('[data-progress]').forEach(el => {
299 el.classList.remove('animate-progress');
300 });
301 }
302
303 const firstProgress = document.querySelector('[data-progress="prs"]');
304 if (firstProgress) {
305 firstProgress.classList.add('animate-progress');
306 }
307
308 document.querySelectorAll('label[for^="feature-"]').forEach(label => {
309 label.addEventListener('click', stopTimer);
310 });
311
312 timerInterval = setInterval(function() {
313 document.querySelectorAll('[data-progress]').forEach(el => {
314 el.classList.remove('animate-progress');
315 void el.offsetWidth; // force reflow
316 });
317
318 currentIndex = (currentIndex + 1) % featureIds.length;
319 document.getElementById(featureIds[currentIndex]).checked = true;
320
321 const activeProgress = document.querySelector('[data-progress="' + progressNames[currentIndex] + '"]');
322 if (activeProgress) {
323 activeProgress.classList.add('animate-progress');
324 }
325 }, 10000);
326 });
327 </script>
328{{ end }}
329
330{{ define "features2" }}
331 {{ $cardStyle := "bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700" }}
332 {{ $cardInnerStyle := "flex flex-row items-center gap-4 md:gap-6 p-4 md:p-6 md:pt-8 relative" }}
333 {{ $contentStyle := "flex-1 flex flex-col" }}
334 {{ $titleStyle := "text-xl md:text-2xl font-bold mb-2 md:mb-3 text-gray-900 dark:text-gray-100" }}
335 {{ $descriptionStyle := "text-gray-600 dark:text-gray-300 text-sm md:text-base leading-relaxed mb-5 md:mb-0" }}
336 {{ $linkMobileStyle := "hover:no-underline inline-flex md:hidden items-center gap-2 text-sm text-black dark:text-white font-medium" }}
337 {{ $iconContainerStyle := "relative shrink-0 w-24 h-24 md:w-48 md:h-48" }}
338 {{ $iconCircleStyle := "rounded-full flex items-center justify-center border border-gray-200 dark:border-gray-700" }}
339 {{ $linkDesktopStyle := "hover:no-underline hidden md:inline-flex absolute -bottom-10 -right-14 items-center gap-2 p-3 text-base btn" }}
340
341 <div class="w-full flex flex-col gap-6 md:gap-40 max-w-5xl mx-auto px-2">
342 <div class="{{ $cardStyle }} md:mr-32">
343 <div class="{{ $cardInnerStyle }}">
344 {{ $moreLink := "https://docs.tangled.org/quick-start-guide.html#login-or-sign-up" }}
345 <div class="{{ $contentStyle }}">
346 <h3 class="{{ $titleStyle }}">Built on AT Protocol</h3>
347 <p class="{{ $descriptionStyle }}">
348 <a class="underline" href="https://atproto.com/">AT Protocol</a> enables federated code-collaboration. Submit
349 pull-requests or bug-reports to any repository hosted on any
350 server.
351 <br>
352 <br>
353 Bring an existing AT Protocol account (such as your Bluesky
354 account), or signup for one with us.
355 </p>
356 <a href="{{ $moreLink }}" class="{{ $linkMobileStyle }}">
357 Learn more
358 {{ i "arrow-right" "size-4" }}
359 </a>
360 </div>
361 <div class="{{ $iconContainerStyle }}">
362 <div class="{{ $iconCircleStyle }} w-full h-full bg-blue-100 dark:bg-blue-900/50">
363 {{ i "at-sign" "size-12 md:size-24" "text-blue-500 dark:text-blue-500" "rotate-12" }}
364 </div>
365 <div class="{{ $iconCircleStyle }} absolute -top-2 -left-2 w-10 h-10 md:w-16 md:h-16 rounded-full bg-white dark:bg-gray-800 -rotate-12">
366 {{ i "globe" "size-4 md:size-8" "text-blue-500 dark:text-blue-500" }}
367 </div>
368 <a href="{{ $moreLink }}" class="{{ $linkDesktopStyle }}">
369 Learn more
370 {{ i "arrow-right" "size-4" }}
371 </a>
372 </div>
373 </div>
374 </div>
375
376 <div class="{{ $cardStyle }} md:ml-32">
377 <div class="{{ $cardInnerStyle }}">
378 {{ $moreLink := "https://tangled.org/core" }}
379 <div class="{{ $contentStyle }}">
380 <h3 class="{{ $titleStyle }}">Free and open source</h3>
381 <p class="{{ $descriptionStyle }}">
382 All of Tangled is open source and built with the community!
383 Check out the <a class="underline" href="https://tangled.org/core">monorepo</a> and join in on the fun.
384 <br>
385 <br>
386 We welcome contributions however big or small. You can start contributing by picking up a
387 <a class="underline" href="https://tangled.org/tangled.org/core/issues?q=state%3Aopen+label%3Agood-first-issue">good-first-issue</a>.
388 </p>
389 <a href="{{ $moreLink }}" class="{{ $linkMobileStyle }}">
390 View source
391 {{ i "arrow-right" "size-4" }}
392 </a>
393 </div>
394 <div class="{{ $iconContainerStyle }}">
395 <div class="{{ $iconCircleStyle }} w-full h-full bg-green-100 dark:bg-green-900/50">
396 {{ i "package-open" "size-12 md:size-24" "text-green-500 dark:text-green-500" "-rotate-12" }}
397 </div>
398 <div class="absolute -top-2 -left-2 w-10 h-10 md:w-16 md:h-16 rounded-full bg-white dark:bg-gray-800 flex items-center justify-center border border-gray-200 dark:border-gray-700 rotate-12">
399 {{ i "heart" "size-4 md:size-8" "text-green-500 dark:text-green-500" }}
400 </div>
401 <a href="{{ $moreLink }}" class="{{ $linkDesktopStyle }}">
402 View source
403 {{ i "arrow-right" "size-4" }}
404 </a>
405 </div>
406 </div>
407 </div>
408
409 <div class="{{ $cardStyle }} md:mr-32">
410 <div class="{{ $cardInnerStyle }}">
411 {{ $moreLink := "/timeline" }}
412 <div class="{{ $contentStyle }}">
413 <h3 class="{{ $titleStyle }}">Social coding is back</h3>
414 <p class="{{ $descriptionStyle }}">
415 Discover trending projects, follow your friends and star your favourite repositories. Coding is better together!
416 </p>
417 <a href="{{ $moreLink }}" class="{{ $linkMobileStyle }}">
418 Explore timeline
419 {{ i "arrow-right" "size-4" }}
420 </a>
421 </div>
422 <div class="{{ $iconContainerStyle }}">
423 <div class="{{ $iconCircleStyle }} w-full h-full bg-amber-100 dark:bg-amber-900/50">
424 {{ i "message-circle-heart" "size-12 md:size-24" "text-amber-500 dark:text-amber-500" "rotate-12" }}
425 </div>
426 <div class="absolute -top-2 -left-2 w-10 h-10 md:w-16 md:h-16 rounded-full bg-white dark:bg-gray-800 flex items-center justify-center border border-gray-200 dark:border-gray-700 -rotate-12">
427 {{ i "users" "size-4 md:size-8" "text-amber-500 dark:text-amber-500" }}
428 </div>
429 <a href="{{ $moreLink }}" class="{{ $linkDesktopStyle }}">
430 Explore timeline
431 {{ i "arrow-right" "size-4" }}
432 </a>
433 </div>
434 </div>
435 </div>
436 </div>
437{{ end }}
438
439{{ define "changelog" }}
440 <div class="w-full px-2 py-16 md:py-24">
441 <h2 class="text-3xl md:text-4xl font-bold mb-8 text-gray-900 dark:text-gray-100">Changelog</h2>
442
443 <div class="grid grid-cols-1 gap-6 mb-6">
444 {{ range $index := list 0 1 2 3 }}
445 <div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 p-6">
446 <div class="flex items-center gap-2 mb-3">
447 <span class="text-xs font-medium text-gray-500 dark:text-gray-400">v1.{{ sub 3 $index }}.0</span>
448 <span class="text-xs text-gray-400 dark:text-gray-500">•</span>
449 <span class="text-xs text-gray-500 dark:text-gray-400">Feb {{ sub 16 $index }}, 2026</span>
450 </div>
451 <h3 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Feature Update {{ sub 4 $index }}</h3>
452 <p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
453 Improvements to the platform including bug fixes, performance enhancements, and new features.
454 </p>
455 </div>
456 {{ end }}
457 </div>
458
459 <div class="flex justify-end">
460 <a href="/changelog" class="hover:no-underline inline-flex items-center gap-2 px-4 py-2 text-base btn">
461 View full changelog
462 {{ i "arrow-right" "size-4" }}
463 </a>
464 </div>
465 </div>
466{{ end }}
467
468{{ define "recentUpdates" }}
469 <div class="w-full px-2 py-16 md:py-24">
470 <h2 class="px-4 text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">Recent updates</h2>
471 <p class="px-4 mb-8 text-gray-500 dark:text-gray-400">
472 Follow <a href="https://bsky.app/profile/tangled.org">@tangled.org</a> on Bluesky for more!
473 </p>
474 <div class="columns-1 md:columns-2 lg:columns-3 gap-6">
475 {{ range $index, $post := .BlueskyPosts }}
476 <div class="{{ if ge $index 3 }}hidden md:block{{ end }}">
477 {{ template "post" $post }}
478 </div>
479 {{ end }}
480 </div>
481 </div>
482{{ end }}
483
484{{ define "post" }}
485 {{ $author := or .AuthorDid "tangled.org" }}
486 <div class="bg-white dark:bg-gray-800 rounded shadow-sm border border-gray-200 dark:border-gray-700 px-6 py-4 flex flex-col gap-2 break-inside-avoid mb-6">
487 <div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
488 <a href="https://bsky.app/profile/{{ $author }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 no-underline hover:no-underline">
489 {{ template "user/fragments/picHandle" $author }}
490 </a>
491 <span>{{ template "repo/fragments/shortTimeAgo" .CreatedAt }}</span>
492 </div>
493 <p class="text-gray-900 dark:text-gray-100 text-base leading-relaxed whitespace-pre-wrap">{{ .Text }}</p>
494
495 {{ if .Embed }}
496 {{ if .Embed.EmbedImages_View }}
497 <div class="grid {{ if eq (len .Embed.EmbedImages_View.Images) 1 }}grid-cols-1{{ else if eq (len .Embed.EmbedImages_View.Images) 2 }}grid-cols-2{{ else }}grid-cols-2{{ end }} gap-2">
498 {{ range .Embed.EmbedImages_View.Images }}
499 <img src="{{ .Fullsize }}" alt="{{ .Alt }}" class="rounded w-full h-auto object-cover border border-gray-200 dark:border-gray-700" loading="lazy" />
500 {{ end }}
501 </div>
502 {{ else if .Embed.EmbedExternal_View }}
503 <a href="{{ .Embed.EmbedExternal_View.External.Uri }}" target="_blank" rel="noopener noreferrer" class="hover:no-underline block border border-gray-200 dark:border-gray-700 rounded overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
504 {{ if .Embed.EmbedExternal_View.External.Thumb }}
505 <img src="{{ .Embed.EmbedExternal_View.External.Thumb }}" alt="" class="w-full h-48 object-cover" loading="lazy" />
506 {{ end }}
507 <div class="p-3">
508 <div class="font-medium text-gray-900 dark:text-gray-100 text-sm mb-1">{{ .Embed.EmbedExternal_View.External.Title }}</div>
509 <div class="text-xs text-gray-600 dark:text-gray-400 line-clamp-2">{{ .Embed.EmbedExternal_View.External.Description }}</div>
510 <div class="text-xs text-gray-500 dark:text-gray-500 mt-1">{{ .Embed.EmbedExternal_View.External.Uri }}</div>
511 </div>
512 </a>
513 {{ else if .Embed.EmbedVideo_View }}
514 <video
515 class="rounded w-full h-auto max-h-[32rem] bg-black border border-gray-200 dark:border-gray-700"
516 controls
517 preload="none"
518 playsinline
519 {{ with .Embed.EmbedVideo_View.Thumbnail }}poster="{{ . }}"{{ end }}
520 {{ with .Embed.EmbedVideo_View.Alt }}aria-label="{{ . }}"{{ end }}
521 data-hls="{{ .Embed.EmbedVideo_View.Playlist }}"
522 ></video>
523 {{ end }}
524 {{ end }}
525
526 <a href="https://bsky.app/profile/{{ $author }}/post/{{ .Rkey }}"
527 target="_blank"
528 rel="noopener noreferrer"
529 class="flex items-center justify-between gap-4 text-sm text-gray-500 dark:text-gray-400 pt-2 no-underline hover:no-underline">
530 <div class="flex items-center gap-4">
531 <div class="flex items-center gap-1">
532 {{ i "heart" "size-4" }}
533 <span>{{ .LikeCount }}</span>
534 </div>
535 <div class="flex items-center gap-1">
536 {{ i "repeat-2" "size-4" }}
537 <span>{{ .RepostCount }}</span>
538 </div>
539 <div class="flex items-center gap-1">
540 {{ i "message-square" "size-4 -scale-x-1" }}
541 <span>{{ add64 .ReplyCount .QuoteCount }}</span>
542 </div>
543 </div>
544 {{ i "arrow-up-right" "size-4" }}
545 </a>
546 </div>
547{{ end }}
548
549{{ define "newsletter" }}
550 <div class="w-full px-2 py-16 md:py-24">
551 <div class="max-w-2xl mx-auto text-center space-y-6">
552 <h2 class="text-3xl md:text-6xl font-bold text-gray-900 dark:text-gray-100">Stay in the loop</h2>
553 <p class="text-xl text-gray-600 dark:text-gray-300">
554 We've got a newsletter! Punch in your email to stay updated on what we're shipping at Tangled.
555 </p>
556 {{ template "timeline/fragments/newsletterForm" (dict "Id" "home" "Variant" "hero") }}
557 </div>
558 </div>
559{{ end }}