mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

1<script setup lang="ts"> 2// If already signed in, bounce to the dashboard. 3const { data: session } = await useFetch('/api/me/whoami', { 4 // Don't blow up the page on the (very common) 401. 5 ignoreResponseError: true, 6}) 7 8if (session.value && typeof session.value === 'object' && 'did' in session.value) { 9 await navigateTo('/dashboard') 10} 11 12const installUrl = 'https://github.com/apps/synchub-to/installations/new' 13 14const heading = useTemplateRef('heading') 15const router = useRouter() 16router.afterEach(() => { 17 nextTick(() => heading.value?.focus({ focusVisible: false })) 18}) 19</script> 20 21<template> 22 <div class="shell"> 23 <a class="skip-link" href="#main">skip to main content</a> 24 <header class="nav-term"> 25 <nav class="nav-term__line" aria-label="primary"><span class="prompt" aria-hidden="true">&gt;</span> <a class="nav-term__mark" href="/" aria-label="synchub.to home"><SynchubMark :wordmark="true" :size="18" /></a> <ul class="nav-term__args"><li><a href="#start">--start</a></li> <li><a href="#signin">--sign-in</a></li></ul><span class="caret" aria-hidden="true"></span></nav> 26 </header> 27 28 <main id="main"> 29 <section class="hero reveal" style="--i: 0"> 30 <h1 ref="heading" tabindex="-1" class="hero__title heading-target">github tangled</h1> 31 <p class="hero__lede"> 32 synchub mirrors your public repositories to 33 <a href="https://tangled.org">tangled.org</a> on every push. 34 install the github app, connect your tangled identity, and your branches 35 and tags will stay in sync. 36 </p> 37 38 <pre class="session" aria-label="How synchub works"><span class="c"># 1 · install the github app on the repos to mirror</span> 39<span class="p">$</span> gh app install <span class="a">synchub-to</span> 40 41<span class="c"># 2 · connect your tangled identity</span> 42<span class="p">$</span> synchub-to <span class="a">@alice.bsky.social</span> 43 44<span class="c"># 3 · push as usual</span> 45<span class="p">$</span> git push origin main 46<span class="ok"></span> mirrored alice/widget tangled.org <span class="dim">(2 refs)</span><span class="caret" aria-hidden="true"></span></pre> 47 </section> 48 49 <section id="start" class="panel reveal" style="--i: 1"> 50 <h2 class="panel__title">new here?</h2> 51 <p class="panel__body"> 52 install the github app on the repositories you'd like to mirror, 53 then come back and sign in with your tangled handle. (only public, 54 non-fork repos are synced.) 55 </p> 56 <a class="btn btn--primary" :href="installUrl">install the github app</a> 57 </section> 58 59 <section id="signin" class="panel reveal" style="--i: 2"> 60 <h2 class="panel__title">returning?</h2> 61 <p class="panel__body"> 62 already connected on another device? just enter your handle. 63 </p> 64 <form class="signin" action="/api/atproto/login" method="get"> 65 <div class="signin__group"> 66 <label class="signin__label" for="handle">handle</label> 67 <span class="signin__field"> 68 <span class="signin__prompt" aria-hidden="true">@</span> 69 <input 70 id="handle" 71 type="text" 72 name="handle" 73 required 74 placeholder="alice.bsky.social" 75 autocomplete="username" 76 autocapitalize="none" 77 autocorrect="off" 78 spellcheck="false" 79 > 80 </span> 81 </div> 82 <button class="btn" type="submit">sign in</button> 83 </form> 84 </section> 85 </main> 86 87 <footer id="how" class="foot-stmt"> 88 <p class="foot-stmt__line">code in sync.</p> 89 <div class="foot-stmt__meta"> 90 <SynchubMark :wordmark="true" :size="18" /> 91 <span class="muted">© 2026 · MIT · <a href="https://github.com/danielroe/synchub.to">source</a></span> 92 </div> 93 </footer> 94 </div> 95</template> 96 97<style scoped> 98.shell { 99 max-width: 52rem; 100 margin: 0 auto; 101 padding-inline: var(--page-gutter); 102} 103 104.nav-term { 105 padding: var(--space-md) 0; 106 border-bottom: var(--rule-hair) solid var(--color-rule); 107} 108 109.nav-term__line { 110 display: flex; 111 align-items: center; 112 flex-wrap: wrap; 113 gap: 0.6ch; 114 font-family: var(--font-mono); 115 font-size: var(--text-sm); 116 margin: 0; 117} 118 119.nav-term__line .prompt { color: var(--color-accent); } 120 121.nav-term__mark { 122 display: inline-flex; 123 text-decoration: none; 124 margin-right: 1.2ch; 125} 126 127.nav-term__mark:hover { color: inherit; } 128 129.nav-term__args { 130 display: inline-flex; 131 align-items: center; 132 gap: 0.9ch; 133 list-style: none; 134 margin: 0; 135 padding: 0; 136} 137 138.nav-term__args a { 139 display: inline-flex; 140 align-items: center; 141 min-height: 44px; 142 color: var(--color-muted); 143 text-decoration: underline; 144 text-underline-offset: 2px; 145 white-space: nowrap; 146} 147 148.nav-term__args a:hover { color: var(--color-ink); } 149 150.caret { 151 display: inline-block; 152 width: 1ch; 153 color: var(--color-accent); 154 animation: blink 1.05s steps(2) infinite; 155} 156 157@keyframes blink { 50% { opacity: 0; } } 158@media (prefers-reduced-motion: reduce) { .caret { animation: none; } } 159 160.hero { 161 padding-block: var(--space-2xl) var(--space-xl); 162} 163 164.heading-target:focus { outline: none; } 165 166.hero__title { 167 font-size: var(--text-display); 168 line-height: 1.04; 169 margin: 0 0 var(--space-lg); 170 overflow-wrap: anywhere; 171 min-width: 0; 172} 173 174.hero__lede { 175 max-width: var(--measure); 176 color: var(--color-muted); 177 font-size: var(--text-md); 178 margin: 0 0 var(--space-xl); 179} 180 181.session { 182 font-family: var(--font-mono); 183 font-size: var(--text-sm); 184 line-height: 1.7; 185 background: var(--color-paper-2); 186 border: var(--rule-hair) solid var(--color-rule); 187 border-radius: var(--radius-md); 188 padding: var(--space-lg); 189 margin: 0; 190 overflow-x: auto; 191 color: var(--color-ink); 192} 193 194.session .c { color: var(--color-neutral); } 195.session .p { color: var(--color-accent); } 196.session .a { color: var(--color-info); } 197.session .ok { color: var(--color-ok); } 198.session .dim { color: var(--color-neutral); } 199 200.panel { 201 padding-block: var(--space-xl); 202 border-top: var(--rule-hair) solid var(--color-rule); 203} 204 205.panel__title { 206 font-size: var(--text-lg); 207 margin: 0 0 var(--space-sm); 208} 209 210.panel__body { 211 max-width: var(--measure); 212 color: var(--color-muted); 213 margin: 0 0 var(--space-lg); 214} 215 216.signin { 217 display: flex; 218 flex-wrap: wrap; 219 align-items: end; 220 gap: var(--space-sm); 221} 222 223.signin__group { 224 display: flex; 225 flex-direction: column; 226 gap: var(--space-2xs); 227} 228 229.signin__label { 230 font-family: var(--font-mono); 231 font-size: var(--text-xs); 232 letter-spacing: 0.06em; 233 text-transform: uppercase; 234 color: var(--color-muted); 235} 236 237.signin__field { 238 display: flex; 239 align-items: center; 240 background: var(--color-paper-2); 241 border: var(--rule-hair) solid var(--color-rule-interactive); 242 border-radius: var(--radius-sm); 243} 244 245.signin__field:focus-within { 246 outline: 2px solid var(--color-focus); 247 outline-offset: 2px; 248 border-color: var(--color-accent); 249} 250 251.signin__prompt { 252 padding-inline: 0.75ch 0; 253 color: var(--color-accent); 254 font-family: var(--font-mono); 255} 256 257.signin input { 258 flex: 1; 259 min-width: 14rem; 260 padding: 0.55rem 0.75rem 0.55rem 0.4ch; 261 border: 0; 262 background: transparent; 263 color: var(--color-ink); 264 font-family: var(--font-mono); 265 font-size: var(--text-base); 266} 267 268.signin input::placeholder { color: var(--color-neutral); } 269 270.btn { 271 display: inline-block; 272 padding: 0.55rem 1.1rem; 273 border: var(--rule-hair) solid var(--color-rule-interactive); 274 border-radius: var(--radius-sm); 275 background: var(--color-paper-2); 276 color: var(--color-ink); 277 font-family: var(--font-mono); 278 font-size: var(--text-sm); 279 text-decoration: none; 280 white-space: nowrap; 281 cursor: pointer; 282 transition: transform var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out); 283} 284 285.btn:hover { border-color: var(--color-accent); transform: translateY(-1px); color: var(--color-ink); } 286.btn:active { transform: translateY(0); } 287 288.btn--primary { 289 border-color: var(--color-accent); 290 color: var(--color-accent); 291} 292 293.foot-stmt { 294 display: grid; 295 gap: var(--space-lg); 296 padding-block: var(--space-2xl) var(--space-xl); 297 border-top: var(--rule-hair) solid var(--color-rule); 298} 299 300.foot-stmt__line { 301 font-family: var(--font-display); 302 font-size: clamp(1.5rem, 4vw, 2.4rem); 303 line-height: 1.05; 304 letter-spacing: -0.02em; 305 max-width: 22ch; 306 margin: 0; 307} 308 309.foot-stmt__meta { 310 display: flex; 311 justify-content: space-between; 312 align-items: center; 313 flex-wrap: wrap; 314 gap: var(--space-sm); 315 padding-top: var(--space-sm); 316 border-top: var(--rule-hair) solid var(--color-rule); 317 font-size: var(--text-sm); 318} 319 320.muted { color: var(--color-neutral); } 321.muted a { color: var(--color-muted); } 322</style>