Fork of Chiri for Astro for my blog
6
fork

Configure Feed

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

New augment blog post along with features that are required for it

+556 -16
+9 -1
astro.config.ts
··· 7 7 import rehypeKatex from 'rehype-katex' 8 8 import remarkEmbeddedMedia from './src/plugins/remark-embedded-media.mjs' 9 9 import remarkReadingTime from './src/plugins/remark-reading-time.mjs' 10 + import remarkFlexibleMarkers from 'remark-flexible-markers'; 10 11 import rehypeCleanup from './src/plugins/rehype-cleanup.mjs' 11 12 import rehypeImageProcessor from './src/plugins/rehype-image-processor.mjs' 12 13 import rehypeCopyCode from './src/plugins/rehype-copy-code.mjs' ··· 31 32 theme: 'css-variables', 32 33 wrap: false 33 34 }, 34 - remarkPlugins: [remarkMath, remarkDirective, remarkEmbeddedMedia, remarkReadingTime, remarkTOC], 35 + remarkPlugins: [ 36 + remarkMath, 37 + remarkDirective, 38 + remarkEmbeddedMedia, 39 + remarkReadingTime, 40 + remarkTOC, 41 + () => (tree) => { console.log('remark running'); return tree; }, 42 + remarkFlexibleMarkers], 35 43 rehypePlugins: [ 36 44 rehypeKatex, 37 45 rehypeCleanup,
+1
package.json
··· 35 35 "rehype-external-links": "^3.0.0", 36 36 "rehype-katex": "^7.0.1", 37 37 "remark-directive": "^4.0.0", 38 + "remark-flexible-markers": "^1.3.3", 38 39 "remark-math": "^6.0.0", 39 40 "sanitize-html": "^2.17.1", 40 41 "sharp": "^0.34.5",
+50
pnpm-lock.yaml
··· 59 59 remark-directive: 60 60 specifier: ^4.0.0 61 61 version: 4.0.0 62 + remark-flexible-markers: 63 + specifier: ^1.3.3 64 + version: 1.3.3(unified@11.0.5) 62 65 remark-math: 63 66 specifier: ^6.0.0 64 67 version: 6.0.0 ··· 3707 3710 remark-directive@4.0.0: 3708 3711 resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==} 3709 3712 3713 + remark-flexible-markers@1.3.3: 3714 + resolution: {integrity: sha512-oIE2YjSXAa4e+lTbkkr1Bd157bxJtlliQciGuBHNJ3Fl9+4+bJWORATwWkBGK4bTsLSffR3UYUg7GUXiY3geXA==} 3715 + peerDependencies: 3716 + unified: ^11 3717 + 3710 3718 remark-gfm@4.0.1: 3711 3719 resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} 3712 3720 ··· 4129 4137 unifont@0.7.4: 4130 4138 resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} 4131 4139 4140 + unist-builder@4.0.0: 4141 + resolution: {integrity: sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==} 4142 + 4132 4143 unist-util-find-after@5.0.0: 4133 4144 resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} 4145 + 4146 + unist-util-find-all-after@5.0.0: 4147 + resolution: {integrity: sha512-nGmOYvTSdGcI4RvrUNfe0mOsqqbbJOtqCQsppsY9KZjmv3nwM3YRgNBwFPdZ8Y+iv9Z/2PDjR9u6u+uK62XTTg==} 4148 + 4149 + unist-util-find-all-before@5.0.0: 4150 + resolution: {integrity: sha512-zir6a7GsXfdn4YAWR4F3hLNKZjTjLBJurdyquysvmX38xbftS1+qwvEhutxxHLq0Pp1tW5V1TDiuj+qluuOnKw==} 4151 + 4152 + unist-util-find-between-all@1.1.6: 4153 + resolution: {integrity: sha512-KTlYGJMu3waM/UiDoyJ374VzIvRRfw5yO/DLuy2vkzTfjJ41HDgAO/Tukzga7imk9l3W688arso3kE+5b+IW6g==} 4134 4154 4135 4155 unist-util-is@6.0.1: 4136 4156 resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} ··· 8872 8892 transitivePeerDependencies: 8873 8893 - supports-color 8874 8894 8895 + remark-flexible-markers@1.3.3(unified@11.0.5): 8896 + dependencies: 8897 + '@types/mdast': 4.0.4 8898 + unified: 11.0.5 8899 + unist-builder: 4.0.0 8900 + unist-util-find-after: 5.0.0 8901 + unist-util-find-all-after: 5.0.0 8902 + unist-util-find-all-before: 5.0.0 8903 + unist-util-find-between-all: 1.1.6 8904 + unist-util-visit: 5.1.0 8905 + 8875 8906 remark-gfm@4.0.1: 8876 8907 dependencies: 8877 8908 '@types/mdast': 4.0.4 ··· 9391 9422 ofetch: 1.5.1 9392 9423 ohash: 2.0.11 9393 9424 9425 + unist-builder@4.0.0: 9426 + dependencies: 9427 + '@types/unist': 3.0.3 9428 + 9394 9429 unist-util-find-after@5.0.0: 9430 + dependencies: 9431 + '@types/unist': 3.0.3 9432 + unist-util-is: 6.0.1 9433 + 9434 + unist-util-find-all-after@5.0.0: 9435 + dependencies: 9436 + '@types/unist': 3.0.3 9437 + unist-util-is: 6.0.1 9438 + 9439 + unist-util-find-all-before@5.0.0: 9440 + dependencies: 9441 + '@types/unist': 3.0.3 9442 + unist-util-is: 6.0.1 9443 + 9444 + unist-util-find-between-all@1.1.6: 9395 9445 dependencies: 9396 9446 '@types/unist': 3.0.3 9397 9447 unist-util-is: 6.0.1
public/assets/augment-logo.png

This is a binary file and will not be displayed.

+51 -12
src/components/widgets/sequoia-comments.js
··· 113 113 align-items: center; 114 114 gap: 0.375rem; 115 115 padding: 0.5rem 1rem; 116 - background: var(--sequoia-accent-color, #2563eb); 117 - color: #ffffff; 118 116 border: none; 119 - border-radius: var(--sequoia-border-radius, 8px); 117 + border-radius: var(--sequoia-border-radius, 15px); 120 118 font-size: 0.875rem; 121 119 font-weight: 500; 122 120 cursor: pointer; 123 121 text-decoration: none; 124 122 transition: background-color 0.15s ease; 123 + margin-left:10px; 125 124 } 126 125 127 - .sequoia-reply-button:hover { 126 + .sequoia-reply-bluesky { 127 + background: var(--sequoia-accent-color, #2563eb); 128 + color: #ffffff; 129 + } 130 + 131 + .sequoia-reply-blacksky { 132 + background: var(--sequoia-accent-color, #6060E9); 133 + color: #ffffff; 134 + } 135 + 136 + .sequoia-reply-bluesky:hover { 128 137 background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black); 138 + } 139 + 140 + .sequoia-reply-blacksky:hover { 141 + background: color-mix(in srgb, var(--sequoia-accent-color, #5252c3) 85%, black); 129 142 } 130 143 131 144 .sequoia-reply-button svg { ··· 548 561 } 549 562 550 563 /** 564 + * Build a Blacksky app URL for a post 565 + * @param {string} postUri - AT Protocol URI for the post 566 + * @returns {string} Blacksky app URL 567 + */ 568 + function buildBlackskyAppUrl(postUri) { 569 + const parsed = parseAtUri(postUri); 570 + if (!parsed) { 571 + throw new Error(`Invalid post URI: ${postUri}`); 572 + } 573 + 574 + return `https://blacksky.community/profile/${parsed.did}/post/${parsed.rkey}`; 575 + } 576 + 577 + /** 551 578 * Type guard for ThreadViewPost 552 579 * @param {any} post - Post to check 553 580 * @returns {boolean} True if post is a ThreadViewPost ··· 563 590 const BLUESKY_ICON = `<svg class="sequoia-bsky-logo" viewBox="0 0 600 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> 564 591 <path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/> 565 592 </svg>`; 593 + const BLACKSKY_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.0620117 0.348442 87.9941 74.9653"><path d="M41.9565 74.9643L24.0161 74.9653L41.9565 74.9643ZM63.8511 74.9653H45.9097L63.8501 74.9643V57.3286H63.8511V74.9653ZM45.9097 44.5893C45.9099 49.2737 49.7077 53.0707 54.3921 53.0707H63.8501V57.3286H54.3921C49.7077 57.3286 45.9099 61.1257 45.9097 65.81V74.9643H41.9565V65.81C41.9563 61.1258 38.1593 57.3287 33.4751 57.3286H24.0161V53.0707H33.4741C38.1587 53.0707 41.9565 49.2729 41.9565 44.5883V35.1303H45.9097V44.5893ZM63.8511 53.0707H63.8501V35.1303H63.8511V53.0707Z" fill="white"></path><path d="M52.7272 9.83198C49.4148 13.1445 49.4148 18.5151 52.7272 21.8275L59.4155 28.5158L56.4051 31.5262L49.7169 24.8379C46.4044 21.5254 41.0338 21.5254 37.7213 24.8379L31.2482 31.3111L28.4527 28.5156L34.9259 22.0424C38.2383 18.7299 38.2383 13.3594 34.9259 10.0469L28.2378 3.35883L31.2482 0.348442L37.9365 7.03672C41.2489 10.3492 46.6195 10.3492 49.932 7.03672L56.6203 0.348442L59.4155 3.14371L52.7272 9.83198Z" fill="white"/><path d="M24.3831 23.2335C23.1706 27.7584 25.8559 32.4095 30.3808 33.6219L39.5172 36.07L38.4154 40.182L29.2793 37.734C24.7544 36.5215 20.1033 39.2068 18.8909 43.7317L16.5215 52.5745L12.7028 51.5513L15.0721 42.7088C16.2846 38.1839 13.5993 33.5328 9.07434 32.3204L-0.0620117 29.8723L1.03987 25.76L10.1762 28.2081C14.7011 29.4206 19.3522 26.7352 20.5647 22.2103L23.0127 13.074L26.8311 14.0971L24.3831 23.2335Z" fill="white"/><path d="M67.3676 22.0297C68.5801 26.5546 73.2311 29.2399 77.756 28.0275L86.8923 25.5794L87.9941 29.6914L78.8578 32.1394C74.3329 33.3519 71.6476 38.003 72.86 42.5279L75.2294 51.3707L71.411 52.3938L69.0417 43.5513C67.8293 39.0264 63.1782 36.3411 58.6533 37.5535L49.5169 40.0016L48.415 35.8894L57.5514 33.4413C62.0763 32.2288 64.7616 27.5778 63.5492 23.0528L61.1011 13.9165L64.9195 12.8934L67.3676 22.0297Z" fill="white"/></svg>'; 566 594 567 595 // ============================================================================ 568 596 // Web Component ··· 660 688 } 661 689 662 690 const postUrl = buildBskyAppUrl(document.bskyPostRef.uri); 691 + const blackskyPostUrl = buildBlackskyAppUrl(document.bskyPostRef.uri); 663 692 664 693 // Fetch the post thread 665 694 const thread = await getPostThread(document.bskyPostRef.uri, this.depth); ··· 668 697 const replies = thread.replies?.filter(isThreadViewPost) ?? []; 669 698 if (replies.length === 0) { 670 699 this.state = { type: "empty", postUrl }; 700 + this.state = { type: "empty", blackskyPostUrl }; 671 701 this.render(); 672 702 return; 673 703 } 674 704 675 705 this.state = { type: "loaded", thread, postUrl }; 706 + this.state = { type: "loaded", thread, blackskyPostUrl }; 676 707 this.render(); 677 708 } catch (error) { 678 709 const message = ··· 716 747 this.commentsContainer.innerHTML = ` 717 748 <div class="sequoia-comments-header"> 718 749 <h3 class="sequoia-comments-title">Comments</h3> 719 - <a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button"> 720 - ${BLUESKY_ICON} 721 - Reply on Bluesky 722 - </a> 750 + <div> 751 + <a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky"> 752 + ${BLUESKY_ICON} 753 + </a> 754 + <a href="${this.state.blackskyPostUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky"> 755 + ${BLACKSKY_ICON} 756 + </a> 757 + </div> 723 758 </div> 724 759 <div class="sequoia-empty"> 725 760 No comments yet. Be the first to reply on Bluesky! ··· 746 781 this.commentsContainer.innerHTML = ` 747 782 <div class="sequoia-comments-header"> 748 783 <h3 class="sequoia-comments-title">${commentCount} Comment${commentCount !== 1 ? "s" : ""}</h3> 749 - <a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button"> 750 - ${BLUESKY_ICON} 751 - Reply on Bluesky 752 - </a> 784 + <div> 785 + <a href="${this.state.postUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-bluesky"> 786 + ${BLUESKY_ICON} 787 + </a> 788 + <a href="${this.state.blackskyPostUrl}" target="_blank" rel="noopener noreferrer" class="sequoia-reply-button sequoia-reply-blacksky"> 789 + ${BLACKSKY_ICON} 790 + </a> 791 + </div> 753 792 </div> 754 793 <div class="sequoia-comments-list"> 755 794 ${threadsHtml}
+379
src/components/widgets/sequoia-subscribe.js
··· 1 + /** 2 + * Sequoia Subscribe - A Bluesky-powered subscribe component 3 + * 4 + * A self-contained Web Component that lets users subscribe to a publication 5 + * via the AT Protocol by creating a site.standard.graph.subscription record. 6 + * 7 + * Usage: 8 + * <sequoia-subscribe></sequoia-subscribe> 9 + * 10 + * The component resolves the publication AT URI from the host site's 11 + * /.well-known/site.standard.publication endpoint. 12 + * 13 + * Attributes: 14 + * - publication-uri: Override the publication AT URI (optional) 15 + * - callback-uri: Redirect URI after OAuth authentication (default: "https://sequoia.pub/subscribe") 16 + * - label: Button label text (default: "Subscribe on Bluesky") 17 + * - hide: Set to "auto" to hide if no publication URI is detected 18 + * 19 + * CSS Custom Properties: 20 + * - --sequoia-fg-color: Text color (default: #1f2937) 21 + * - --sequoia-bg-color: Background color (default: #ffffff) 22 + * - --sequoia-border-color: Border color (default: #e5e7eb) 23 + * - --sequoia-accent-color: Accent/button color (default: #2563eb) 24 + * - --sequoia-secondary-color: Secondary text color (default: #6b7280) 25 + * - --sequoia-border-radius: Border radius (default: 8px) 26 + * 27 + * Events: 28 + * - sequoia-subscribed: Fired when the subscription is created successfully. 29 + * detail: { publicationUri: string, recordUri: string } 30 + * - sequoia-subscribe-error: Fired when the subscription fails. 31 + * detail: { message: string } 32 + */ 33 + 34 + // ============================================================================ 35 + // Styles 36 + // ============================================================================ 37 + 38 + const styles = ` 39 + :host { 40 + display: inline-block; 41 + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 42 + color: var(--sequoia-fg-color, #1f2937); 43 + line-height: 1.5; 44 + } 45 + 46 + * { 47 + box-sizing: border-box; 48 + } 49 + 50 + .sequoia-subscribe-button { 51 + display: inline-flex; 52 + align-items: center; 53 + gap: 0.375rem; 54 + padding: 0.5rem 1rem; 55 + background: var(--sequoia-accent-color, #2563eb); 56 + color: #ffffff; 57 + border: none; 58 + border-radius: var(--sequoia-border-radius, 8px); 59 + font-size: 0.875rem; 60 + font-weight: 500; 61 + cursor: pointer; 62 + text-decoration: none; 63 + transition: background-color 0.15s ease; 64 + font-family: inherit; 65 + } 66 + 67 + .sequoia-subscribe-button:hover:not(:disabled) { 68 + background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black); 69 + } 70 + 71 + .sequoia-subscribe-button:disabled { 72 + opacity: 0.6; 73 + cursor: not-allowed; 74 + } 75 + 76 + .sequoia-subscribe-button svg { 77 + width: 1rem; 78 + height: 1rem; 79 + flex-shrink: 0; 80 + } 81 + 82 + .sequoia-subscribe-button--success { 83 + background: #16a34a; 84 + } 85 + 86 + .sequoia-subscribe-button--success:hover:not(:disabled) { 87 + background: color-mix(in srgb, #16a34a 85%, black); 88 + } 89 + 90 + .sequoia-loading-spinner { 91 + display: inline-block; 92 + width: 1rem; 93 + height: 1rem; 94 + border: 2px solid rgba(255, 255, 255, 0.4); 95 + border-top-color: #ffffff; 96 + border-radius: 50%; 97 + animation: sequoia-spin 0.8s linear infinite; 98 + flex-shrink: 0; 99 + } 100 + 101 + @keyframes sequoia-spin { 102 + to { transform: rotate(360deg); } 103 + } 104 + 105 + .sequoia-error-message { 106 + display: inline-block; 107 + font-size: 0.8125rem; 108 + color: #dc2626; 109 + margin-top: 0.375rem; 110 + } 111 + `; 112 + 113 + // ============================================================================ 114 + // Icons 115 + // ============================================================================ 116 + 117 + const BLUESKY_ICON = `<svg class="sequoia-bsky-logo" viewBox="0 0 600 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> 118 + <path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/> 119 + </svg>`; 120 + 121 + const CHECK_ICON = `<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> 122 + <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/> 123 + </svg>`; 124 + 125 + // ============================================================================ 126 + // AT Protocol Functions 127 + // ============================================================================ 128 + 129 + /** 130 + * Fetch the publication AT URI from the host site's well-known endpoint. 131 + * @param {string} [origin] - Origin to fetch from (defaults to current page origin) 132 + * @returns {Promise<string>} Publication AT URI 133 + */ 134 + async function fetchPublicationUri(origin) { 135 + const base = origin ?? window.location.origin; 136 + const url = `${base}/.well-known/site.standard.publication`; 137 + const response = await fetch(url); 138 + if (!response.ok) { 139 + throw new Error(`Could not fetch publication URI: ${response.status}`); 140 + } 141 + 142 + // Accept either plain text (the AT URI itself) or JSON with a `uri` field. 143 + const contentType = response.headers.get("content-type") ?? ""; 144 + if (contentType.includes("application/json")) { 145 + const data = await response.json(); 146 + const uri = data?.uri ?? data?.atUri ?? data?.publication; 147 + if (!uri) { 148 + throw new Error("Publication response did not contain a URI"); 149 + } 150 + return uri; 151 + } 152 + 153 + const text = (await response.text()).trim(); 154 + if (!text.startsWith("at://")) { 155 + throw new Error(`Unexpected publication URI format: ${text}`); 156 + } 157 + return text; 158 + } 159 + 160 + // ============================================================================ 161 + // Web Component 162 + // ============================================================================ 163 + 164 + // SSR-safe base class - use HTMLElement in browser, empty class in Node.js 165 + const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {}; 166 + 167 + class SequoiaSubscribe extends BaseElement { 168 + constructor() { 169 + super(); 170 + const shadow = this.attachShadow({ mode: "open" }); 171 + 172 + const styleTag = document.createElement("style"); 173 + styleTag.innerText = styles; 174 + shadow.appendChild(styleTag); 175 + 176 + const wrapper = document.createElement("div"); 177 + shadow.appendChild(wrapper); 178 + wrapper.part = "container"; 179 + 180 + this.wrapper = wrapper; 181 + this.state = { type: "idle" }; 182 + this.abortController = null; 183 + this.render(); 184 + } 185 + 186 + static get observedAttributes() { 187 + return ["publication-uri", "callback-uri", "label", "hide"]; 188 + } 189 + 190 + connectedCallback() { 191 + // Pre-check publication availability so hide="auto" can take effect 192 + if (!this.publicationUri) { 193 + this.checkPublication(); 194 + } 195 + } 196 + 197 + disconnectedCallback() { 198 + this.abortController?.abort(); 199 + } 200 + 201 + attributeChangedCallback() { 202 + // Reset to idle if attributes change after an error or success 203 + if ( 204 + this.state.type === "error" || 205 + this.state.type === "subscribed" || 206 + this.state.type === "no-publication" 207 + ) { 208 + this.state = { type: "idle" }; 209 + } 210 + this.render(); 211 + } 212 + 213 + get publicationUri() { 214 + return this.getAttribute("publication-uri") ?? null; 215 + } 216 + 217 + get callbackUri() { 218 + return this.getAttribute("callback-uri") ?? "https://sequoia.pub/subscribe"; 219 + } 220 + 221 + get label() { 222 + return this.getAttribute("label") ?? "Subscribe on Bluesky"; 223 + } 224 + 225 + get hide() { 226 + const hideAttr = this.getAttribute("hide"); 227 + return hideAttr === "auto"; 228 + } 229 + 230 + async checkPublication() { 231 + this.abortController?.abort(); 232 + this.abortController = new AbortController(); 233 + 234 + try { 235 + await fetchPublicationUri(); 236 + } catch { 237 + this.state = { type: "no-publication" }; 238 + this.render(); 239 + } 240 + } 241 + 242 + async handleClick() { 243 + if (this.state.type === "loading" || this.state.type === "subscribed") { 244 + return; 245 + } 246 + 247 + this.state = { type: "loading" }; 248 + this.render(); 249 + 250 + try { 251 + const publicationUri = 252 + this.publicationUri ?? (await fetchPublicationUri()); 253 + 254 + // POST to the callbackUri (e.g. https://sequoia.pub/subscribe). 255 + // If the server reports the user isn't authenticated it returns a 256 + // subscribeUrl for the full-page OAuth + subscription flow. 257 + const response = await fetch(this.callbackUri, { 258 + method: "POST", 259 + headers: { "Content-Type": "application/json" }, 260 + credentials: "include", 261 + body: JSON.stringify({ publicationUri }), 262 + }); 263 + 264 + const data = await response.json(); 265 + 266 + if (response.status === 401 && data.authenticated === false) { 267 + // Redirect to the hosted subscribe page to complete OAuth 268 + window.location.href = data.subscribeUrl; 269 + return; 270 + } 271 + 272 + if (!response.ok) { 273 + throw new Error(data.error ?? `HTTP ${response.status}`); 274 + } 275 + 276 + const { recordUri } = data; 277 + this.state = { type: "subscribed", recordUri, publicationUri }; 278 + this.render(); 279 + 280 + this.dispatchEvent( 281 + new CustomEvent("sequoia-subscribed", { 282 + bubbles: true, 283 + composed: true, 284 + detail: { publicationUri, recordUri }, 285 + }), 286 + ); 287 + } catch (error) { 288 + // Don't overwrite state if we already navigated away 289 + if (this.state.type !== "loading") return; 290 + 291 + const message = 292 + error instanceof Error ? error.message : "Failed to subscribe"; 293 + this.state = { type: "error", message }; 294 + this.render(); 295 + 296 + this.dispatchEvent( 297 + new CustomEvent("sequoia-subscribe-error", { 298 + bubbles: true, 299 + composed: true, 300 + detail: { message }, 301 + }), 302 + ); 303 + } 304 + } 305 + 306 + render() { 307 + const { type } = this.state; 308 + 309 + if (type === "no-publication") { 310 + if (this.hide) { 311 + this.wrapper.innerHTML = ""; 312 + this.wrapper.style.display = "none"; 313 + } 314 + return; 315 + } 316 + 317 + const isLoading = type === "loading"; 318 + const isSubscribed = type === "subscribed"; 319 + 320 + const icon = isLoading 321 + ? `<span class="sequoia-loading-spinner"></span>` 322 + : isSubscribed 323 + ? CHECK_ICON 324 + : BLUESKY_ICON; 325 + 326 + const label = isSubscribed ? "Subscribed" : this.label; 327 + const buttonClass = [ 328 + "sequoia-subscribe-button", 329 + isSubscribed ? "sequoia-subscribe-button--success" : "", 330 + ] 331 + .filter(Boolean) 332 + .join(" "); 333 + 334 + const errorHtml = 335 + type === "error" 336 + ? `<span class="sequoia-error-message">${escapeHtml(this.state.message)}</span>` 337 + : ""; 338 + 339 + this.wrapper.innerHTML = ` 340 + <button 341 + class="${buttonClass}" 342 + type="button" 343 + part="button" 344 + ${isLoading || isSubscribed ? "disabled" : ""} 345 + aria-label="${isSubscribed ? "Subscribed" : this.label}" 346 + > 347 + ${icon} 348 + ${label} 349 + </button> 350 + ${errorHtml} 351 + `; 352 + 353 + if (type !== "subscribed") { 354 + const btn = this.wrapper.querySelector("button"); 355 + btn?.addEventListener("click", () => this.handleClick()); 356 + } 357 + } 358 + } 359 + 360 + /** 361 + * Escape HTML special characters (no DOM dependency for SSR). 362 + * @param {string} text 363 + * @returns {string} 364 + */ 365 + function escapeHtml(text) { 366 + return text 367 + .replace(/&/g, "&amp;") 368 + .replace(/</g, "&lt;") 369 + .replace(/>/g, "&gt;") 370 + .replace(/"/g, "&quot;"); 371 + } 372 + 373 + // Register the custom element 374 + if (typeof customElements !== "undefined") { 375 + customElements.define("sequoia-subscribe", SequoiaSubscribe); 376 + } 377 + 378 + // Export for module usage 379 + export { SequoiaSubscribe };
+2 -1
src/config.ts
··· 33 33 toc: true, // Show table of contents (when there is enough page width) 34 34 imageViewer: true, // Enable image viewer 35 35 copyCode: true, // Enable copy button in code blocks 36 - linkCard: true // Enable link card 36 + linkCard: true, // Enable link card 37 + markers: true // Enable highlights on post 37 38 } 38 39 }
+1 -1
src/content/about/about.md
··· 6 6 7 7 augment is a space where I write essays about technology, mostly focused on the open social web and the human internet. 8 8 9 - You can subscribe via [email](https://buttondown.com/augment), [rss](https://augment.ink/rss), [atproto](), or [activitypub](). 9 + You can subscribe via [email](https://buttondown.com/augment), [RSS](https://augment.ink/rss), [the Atmosphere](https://sequoia.pub/subscribe?publicationUri=at%3A%2F%2Fdid%3Aplc%3Axgvzy7ni6ig6ievcbls5jaxe%2Fsite.standard.publication%2F3mgfwckliwc2m), or (soon) ActivityPub.
+56
src/content/posts/augments-atmospheric-home.md
··· 1 + --- 2 + title: augment's Atmospheric Home 3 + pubDate: 2026-03-10 4 + image: /assets/augment-logo.png 5 + tags: 6 + - at protocol 7 + - atmosphere 8 + - atproto 9 + - bluesky 10 + - open social web 11 + - social media 12 + - social web 13 + - blacksky 14 + --- 15 + We made it! augment has officially moved over to a self-hosted site, and I'm so excited to tell you all about it. 16 + 17 + I've always wanted augment to be a space that I could write, but more importantly, I wanted to be a canvas where I could imagine what blogging can look like when it becomes a ==social space==. One where published posts don't just sit to be seen, but commented on, interacted with directly, and become a portal to spaces where it's being shared so you can discover more. 18 + 19 + I don't want it to be a place you arrive; I want it to be ==a place that can expand that takes you to other places==. 20 + 21 + ### An Atmospheric Blog 22 + In an essay I wrote recently, I spoke about an ["Everything Account"](https://augment.ink/the-everything-account) and how it lives in an ecosystem of services called the Atmosphere. While that focused on the end-user experience, one other component of the Atmosphere is that all the data created in it lives in an accessible space that anyone can pull from. 23 + 24 + This means I can publish things like blog posts into the Atmosphere, and then I can keep track of different services people are using to interact with them. 25 + * I can look at posts and comments being created on microblogs like [Bluesky](https://bsky.app) and [Blacksky](https://blacksky.community) and have them at the end of blog posts so you can engage with them 26 + * I can peek at collections it's being added to on services like [semble.so](https://semble.so) and show them here so you can see what else those collections contain, and follow them if you want to 27 + * I can display annotations that are being added to it on [margin.at](https://margin.at) and [seams.so](https://seams.so) and add them alongside this post so you can find insightful readers and follow them for more 28 + * I can link macroblogs on [Leaflet](https://leaflet.pub), [pckt](pckt.blog), [Offprint](https://offprint.app/), and [GreenGale](https://greengale.app/) that mention my blog posts or my posts in different reading experiences like [Skyreader](https://skyreader.app/) 29 + 30 + Over time, as the Atmosphere grows with more services, I can entangle this blog with them. I can make this a living, breathing website that grows with everyone who interacts with it across the open social web. 31 + 32 + To begin this work, I'm starting with having every essay I write here publish to the Atmosphere using [standard.site](https://standard.site/). This makes all of my posts native to the ecosystem, allows readers to subscribe using their Atmosphere account, and lets the them find it on macroblogging services across the Atmosphere. You'll also see comments below this based on who's replying to my microblog announcing the post on apps like Bluesky and Blacksky. Right now, you have to go to a platform to reply; eventually, I want readers to be able to reply directly below the blog post using their Atmosphere account. More on that soon. 33 + 34 + The exciting part about this is that we're also adding standard.site into [Bridgy Fed](https://fed.brid.gy/). This means they'll soon show up on ActivityPub-based services like [Mastodon](https://joinmastodon.org/), [WordPress](https://wordpress.org/), [Ghost](http://ghost.org/), [NodeBB](https://nodebb.org/), and so many more, and I can pull bridged comments from that ecosystem onto this site as well. 35 + 36 + ==augment is an Atmospheric blog that's tapping into the wider open social web==, and it's only just the starting point. We can go so much deeper, and I'm looking forward to experimenting with how deep we can go. 37 + 38 + ### An Open Foundation 39 + But this didn't come from scratch. The new augment lives on the work of multiple projects, and I want to take a moment to call those out. 40 + * The site is forked from [Chiri](https://github.com/the3ash/astro-chiri/), an Astro theme that I ever-so-slightly customized to my needs 41 + * The newsletter is now distributed via [Buttondown](http://buttondown.com/), an email service that simply takes my RSS feed updates and sends them to your inbox 42 + * [standard.site](https://standard.site) is a longform standard built by the Atmosphere longform community, kicked off by Offprint, Leaflet, and pckt 43 + * The standard.site integration is setup using [Sequoia](https://sequoia.pub/), a CLI tool that enables subcriptions, sends the blog post and a microblog to the Atmosphere when it's published, and brings microblog comments back to this page so other readers can see it 44 + 45 + None of this could've been possible without the hard work of the people behind these projects. 46 + 47 + I'm also open sourcing this blog on [GitHub](https://github.com/quillmatiq/augment) and [Tangled](https://tangled.org/quillmatiq.com/augment), a GitHub competitor built on atproto where I'll eventually host the repo myself. That means that as I add new features and make it more Atmospheric, you'll be able to see how I've done it, and can either use that code or use it as inspiration to do the same. 48 + 49 + ### Wherever You Read Your Blogs 50 + You've probably heard the words ["wherever you get your podcasts"](https://www.anildash.com/2024/02/05/wherever-you-get-podcasts/) a lot. It's [a common starting point](https://knotbin.leaflet.pub/3lx3uqveyj22f) for folks to understand open standards. 51 + 52 + Well, starting today, you can read augment wherever you read your blogs: your [email inbox](https://buttondown.com/augment), your [RSS reader](https://augment.ink/rss), the [Atmosphere](https://sequoia.pub/subscribe?publicationUri=at%3A%2F%2Fdid%3Aplc%3Axgvzy7ni6ig6ievcbls5jaxe%2Fsite.standard.publication%2F3mgfwckliwc2m), and (soon) the Fediverse. 53 + 54 + Subscribe where you want to read it. And hopefully, over time, I'll make it worth you while to come here, because it'll have so much more than just my ramblings. 55 + 56 + Welcome to the new Atmospheric home of augment. I'm so excited to show you more.
+1
src/layouts/PostLayout.astro
··· 55 55 <article class="content"> 56 56 <slot /> 57 57 </article> 58 + 58 59 <h2>Comments</h2> 59 60 <sequoia-comments /> 60 61 <script>import '@/components/widgets/sequoia-comments.js'</script>
+4
src/styles/global.css
··· 205 205 clip-path: inset(50%); 206 206 white-space: nowrap; 207 207 } 208 + 209 + mark { 210 + background-color: #fef08a; 211 + }
+1 -1
src/styles/post.css
··· 579 579 /* Spotify */ 580 580 .prose iframe[src*='spotify.com'] { 581 581 aspect-ratio: auto; 582 - } 582 + }
+1
src/types/config.types.ts
··· 34 34 imageViewer: boolean 35 35 copyCode: boolean 36 36 linkCard: boolean 37 + markers: boolean 37 38 } 38 39 39 40 // Theme configuration type