A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing

Automatically redirect to referring page

Also fixes a bug I just noticed in the Subscribe help page.

authored by

Heath Stewart and committed by tangled.org 13c684c0 ec1b55af

+70 -9
+1 -1
docs/docs/pages/subscribe.mdx
··· 147 147 148 148 The component dispatches custom events you can listen to: 149 149 150 - | Event | Description | `detail` | 150 + | Event | Description | Detail | 151 151 |-------|-------------|----------| 152 152 | `sequoia-subscribed` | Fired when the subscription is created successfully. | `{ publicationUri: string, recordUri: string }` | 153 153 | `sequoia-subscribe-error` | Fired when the subscription fails. | `{ message: string }` |
+69 -8
docs/src/routes/subscribe.ts
··· 35 35 const subscribe = new Hono<{ Bindings: Env }>(); 36 36 37 37 const COLLECTION = "site.standard.graph.subscription"; 38 + const REDIRECT_DELAY_SECONDS = 5; 38 39 39 40 // ============================================================================ 40 41 // Helpers ··· 162 163 ); 163 164 } 164 165 166 + // Prefer an explicit returnTo query param (survives the OAuth round-trip); 167 + // fall back to the Referer header on the first visit, ignoring self-referrals. 168 + const referer = c.req.header("referer"); 169 + const returnTo = 170 + c.req.query("returnTo") ?? 171 + (referer && !referer.includes("/subscribe") ? referer : undefined); 172 + 165 173 const did = getSessionDid(c); 166 174 if (!did) { 167 - return c.html(renderHandleForm(publicationUri, styleHref)); 175 + return c.html(renderHandleForm(publicationUri, styleHref, returnTo)); 168 176 } 169 177 170 178 try { ··· 179 187 ); 180 188 if (existingUri) { 181 189 return c.html( 182 - renderSuccess(publicationUri, existingUri, true, styleHref), 190 + renderSuccess(publicationUri, existingUri, true, styleHref, returnTo), 183 191 ); 184 192 } 185 193 ··· 193 201 }); 194 202 195 203 return c.html( 196 - renderSuccess(publicationUri, result.data.uri, false, styleHref), 204 + renderSuccess( 205 + publicationUri, 206 + result.data.uri, 207 + false, 208 + styleHref, 209 + returnTo, 210 + ), 197 211 ); 198 212 } catch (error) { 199 213 console.error("Subscribe GET error:", error); ··· 202 216 renderHandleForm( 203 217 publicationUri, 204 218 styleHref, 219 + returnTo, 205 220 "Session expired. Please sign in again.", 206 221 ), 207 222 ); ··· 219 234 const body = await c.req.parseBody(); 220 235 const handle = (body["handle"] as string | undefined)?.trim(); 221 236 const publicationUri = body["publicationUri"] as string | undefined; 237 + const formReturnTo = (body["returnTo"] as string | undefined) || undefined; 222 238 223 239 if (!handle || !publicationUri) { 224 240 const styleHref = await getVocsStyleHref(c.env.ASSETS, c.req.url); ··· 228 244 ); 229 245 } 230 246 231 - const returnTo = `${c.env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}`; 247 + const returnTo = 248 + `${c.env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}` + 249 + (formReturnTo ? `&returnTo=${encodeURIComponent(formReturnTo)}` : ""); 232 250 setReturnToCookie(c, returnTo, c.env.CLIENT_URL); 233 251 234 252 return c.redirect( ··· 243 261 function renderHandleForm( 244 262 publicationUri: string, 245 263 styleHref: string, 264 + returnTo?: string, 246 265 error?: string, 247 266 ): string { 248 267 const errorHtml = error 249 268 ? `<p class="vocs_Paragraph error">${escapeHtml(error)}</p>` 269 + : ""; 270 + const returnToInput = returnTo 271 + ? `<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />` 250 272 : ""; 251 273 252 274 return page( ··· 255 277 <p class="vocs_Paragraph">Enter your Bluesky handle to subscribe to this publication.</p> 256 278 ${errorHtml} 257 279 <form method="POST" action="/subscribe/login"> 258 - <input type="hidden" name="publicationUri" value="${escapeHtml(publicationUri)}" /> 280 + <input type="hidden" name="publicationUri" value="${escapeHtml(publicationUri)}" /> 281 + ${returnToInput} 259 282 <input 260 283 type="text" 261 284 name="handle" ··· 276 299 recordUri: string, 277 300 existing: boolean, 278 301 styleHref: string, 302 + returnTo?: string, 279 303 ): string { 280 304 const msg = existing 281 305 ? "You're already subscribed to this publication." 282 306 : "You've successfully subscribed!"; 283 307 const escapedPublicationUri = escapeHtml(publicationUri); 284 308 const escapedRecordUri = escapeHtml(recordUri); 309 + 310 + const redirectHtml = returnTo 311 + ? `<p class="vocs_Paragraph" id="redirect-msg">Redirecting to <a class="vocs_Anchor" href="${escapeHtml(returnTo)}">${escapeHtml(returnTo)}</a> in <span id="countdown">${REDIRECT_DELAY_SECONDS}</span>\u00a0seconds\u2026</p> 312 + <script> 313 + (function(){ 314 + var secs = ${REDIRECT_DELAY_SECONDS}; 315 + var el = document.getElementById('countdown'); 316 + var iv = setInterval(function(){ 317 + secs--; 318 + if (el) el.textContent = String(secs); 319 + if (secs <= 0) { clearInterval(iv); location.href = ${JSON.stringify(returnTo)}; } 320 + }, 1000); 321 + })(); 322 + </script>` 323 + : ""; 324 + const headExtra = returnTo 325 + ? `<meta http-equiv="refresh" content="${REDIRECT_DELAY_SECONDS};url=${escapeHtml(returnTo)}" />` 326 + : ""; 327 + 285 328 return page( 286 329 ` 287 330 <h1 class="vocs_H1 vocs_Heading">Subscribed ✓</h1> 288 331 <p class="vocs_Paragraph">${msg}</p> 289 - <p class="vocs_Paragraph"><small>Publication: <code class="vocs_Code"><a href="https://pds.ls/${escapedPublicationUri}">${escapedPublicationUri}</a></code></small></p> 290 - <p class="vocs_Paragraph"><small>Record: <code class="vocs_Code"><a href="https://pds.ls/${escapedRecordUri}">${escapedRecordUri}</a></code></small></p> 332 + ${redirectHtml} 333 + <table class="vocs_Table" style="display:table;table-layout:fixed;width:100%;overflow:hidden;"> 334 + <colgroup><col style="width:7rem;"><col></colgroup> 335 + <tbody> 336 + <tr class="vocs_TableRow"> 337 + <td class="vocs_TableCell">Publication</td> 338 + <td class="vocs_TableCell" style="overflow:hidden;"> 339 + <div style="overflow-x:auto;white-space:nowrap;"><code class="vocs_Code"><a href="https://pds.ls/${escapedPublicationUri}">${escapedPublicationUri}</a></code></div> 340 + </td> 341 + </tr> 342 + <tr class="vocs_TableRow"> 343 + <td class="vocs_TableCell">Record</td> 344 + <td class="vocs_TableCell" style="overflow:hidden;"> 345 + <div style="overflow-x:auto;white-space:nowrap;"><code class="vocs_Code"><a href="https://pds.ls/${escapedRecordUri}">${escapedRecordUri}</a></code></div> 346 + </td> 347 + </tr> 348 + </tbody> 349 + </table> 291 350 `, 292 351 styleHref, 352 + headExtra, 293 353 ); 294 354 } 295 355 ··· 300 360 ); 301 361 } 302 362 303 - function page(body: string, styleHref: string): string { 363 + function page(body: string, styleHref: string, headExtra = ""): string { 304 364 return `<!DOCTYPE html> 305 365 <html lang="en"> 306 366 <head> ··· 309 369 <title>Sequoia · Subscribe</title> 310 370 <link rel="stylesheet" href="${styleHref}" /> 311 371 <script>if(window.matchMedia('(prefers-color-scheme: dark)').matches)document.documentElement.classList.add('dark')</script> 372 + ${headExtra} 312 373 <style> 313 374 .page-container { 314 375 max-width: calc(var(--vocs-content_width, 480px) / 1.6);