···11+/**
22+ * Sequoia Subscribe - A Bluesky-powered subscribe component
33+ *
44+ * A self-contained Web Component that lets users subscribe to a publication
55+ * via the AT Protocol by creating a site.standard.graph.subscription record.
66+ *
77+ * Usage:
88+ * <sequoia-subscribe></sequoia-subscribe>
99+ *
1010+ * The component resolves the publication AT URI from the host site's
1111+ * /.well-known/site.standard.publication endpoint.
1212+ *
1313+ * Attributes:
1414+ * - publication-uri: Override the publication AT URI (optional)
1515+ * - callback-uri: Redirect URI after OAuth authentication (default: "https://sequoia.pub/subscribe")
1616+ * - label: Button label text (default: "Subscribe on Bluesky")
1717+ * - hide: Set to "auto" to hide if no publication URI is detected
1818+ *
1919+ * CSS Custom Properties:
2020+ * - --sequoia-fg-color: Text color (default: #1f2937)
2121+ * - --sequoia-bg-color: Background color (default: #ffffff)
2222+ * - --sequoia-border-color: Border color (default: #e5e7eb)
2323+ * - --sequoia-accent-color: Accent/button color (default: #2563eb)
2424+ * - --sequoia-secondary-color: Secondary text color (default: #6b7280)
2525+ * - --sequoia-border-radius: Border radius (default: 8px)
2626+ *
2727+ * Events:
2828+ * - sequoia-subscribed: Fired when the subscription is created successfully.
2929+ * detail: { publicationUri: string, recordUri: string }
3030+ * - sequoia-subscribe-error: Fired when the subscription fails.
3131+ * detail: { message: string }
3232+ */
3333+3434+// ============================================================================
3535+// Styles
3636+// ============================================================================
3737+3838+const styles = `
3939+:host {
4040+ display: inline-block;
4141+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4242+ color: var(--sequoia-fg-color, #1f2937);
4343+ line-height: 1.5;
4444+}
4545+4646+* {
4747+ box-sizing: border-box;
4848+}
4949+5050+.sequoia-subscribe-button {
5151+ display: inline-flex;
5252+ align-items: center;
5353+ gap: 0.375rem;
5454+ padding: 0.5rem 1rem;
5555+ background: var(--sequoia-accent-color, #2563eb);
5656+ color: #ffffff;
5757+ border: none;
5858+ border-radius: var(--sequoia-border-radius, 8px);
5959+ font-size: 0.875rem;
6060+ font-weight: 500;
6161+ cursor: pointer;
6262+ text-decoration: none;
6363+ transition: background-color 0.15s ease;
6464+ font-family: inherit;
6565+}
6666+6767+.sequoia-subscribe-button:hover:not(:disabled) {
6868+ background: color-mix(in srgb, var(--sequoia-accent-color, #2563eb) 85%, black);
6969+}
7070+7171+.sequoia-subscribe-button:disabled {
7272+ opacity: 0.6;
7373+ cursor: not-allowed;
7474+}
7575+7676+.sequoia-subscribe-button svg {
7777+ width: 1rem;
7878+ height: 1rem;
7979+ flex-shrink: 0;
8080+}
8181+8282+.sequoia-subscribe-button--success {
8383+ background: #16a34a;
8484+}
8585+8686+.sequoia-subscribe-button--success:hover:not(:disabled) {
8787+ background: color-mix(in srgb, #16a34a 85%, black);
8888+}
8989+9090+.sequoia-loading-spinner {
9191+ display: inline-block;
9292+ width: 1rem;
9393+ height: 1rem;
9494+ border: 2px solid rgba(255, 255, 255, 0.4);
9595+ border-top-color: #ffffff;
9696+ border-radius: 50%;
9797+ animation: sequoia-spin 0.8s linear infinite;
9898+ flex-shrink: 0;
9999+}
100100+101101+@keyframes sequoia-spin {
102102+ to { transform: rotate(360deg); }
103103+}
104104+105105+.sequoia-error-message {
106106+ display: inline-block;
107107+ font-size: 0.8125rem;
108108+ color: #dc2626;
109109+ margin-top: 0.375rem;
110110+}
111111+`;
112112+113113+// ============================================================================
114114+// Icons
115115+// ============================================================================
116116+117117+const BLUESKY_ICON = `<svg class="sequoia-bsky-logo" viewBox="0 0 600 530" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
118118+ <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"/>
119119+</svg>`;
120120+121121+const CHECK_ICON = `<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
122122+ <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"/>
123123+</svg>`;
124124+125125+// ============================================================================
126126+// AT Protocol Functions
127127+// ============================================================================
128128+129129+/**
130130+ * Fetch the publication AT URI from the host site's well-known endpoint.
131131+ * @param {string} [origin] - Origin to fetch from (defaults to current page origin)
132132+ * @returns {Promise<string>} Publication AT URI
133133+ */
134134+async function fetchPublicationUri(origin) {
135135+ const base = origin ?? window.location.origin;
136136+ const url = `${base}/.well-known/site.standard.publication`;
137137+ const response = await fetch(url);
138138+ if (!response.ok) {
139139+ throw new Error(`Could not fetch publication URI: ${response.status}`);
140140+ }
141141+142142+ // Accept either plain text (the AT URI itself) or JSON with a `uri` field.
143143+ const contentType = response.headers.get("content-type") ?? "";
144144+ if (contentType.includes("application/json")) {
145145+ const data = await response.json();
146146+ const uri = data?.uri ?? data?.atUri ?? data?.publication;
147147+ if (!uri) {
148148+ throw new Error("Publication response did not contain a URI");
149149+ }
150150+ return uri;
151151+ }
152152+153153+ const text = (await response.text()).trim();
154154+ if (!text.startsWith("at://")) {
155155+ throw new Error(`Unexpected publication URI format: ${text}`);
156156+ }
157157+ return text;
158158+}
159159+160160+// ============================================================================
161161+// Web Component
162162+// ============================================================================
163163+164164+// SSR-safe base class - use HTMLElement in browser, empty class in Node.js
165165+const BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {};
166166+167167+class SequoiaSubscribe extends BaseElement {
168168+ constructor() {
169169+ super();
170170+ const shadow = this.attachShadow({ mode: "open" });
171171+172172+ const styleTag = document.createElement("style");
173173+ styleTag.innerText = styles;
174174+ shadow.appendChild(styleTag);
175175+176176+ const wrapper = document.createElement("div");
177177+ shadow.appendChild(wrapper);
178178+ wrapper.part = "container";
179179+180180+ this.wrapper = wrapper;
181181+ this.state = { type: "idle" };
182182+ this.abortController = null;
183183+ this.render();
184184+ }
185185+186186+ static get observedAttributes() {
187187+ return ["publication-uri", "callback-uri", "label", "hide"];
188188+ }
189189+190190+ connectedCallback() {
191191+ // Pre-check publication availability so hide="auto" can take effect
192192+ if (!this.publicationUri) {
193193+ this.checkPublication();
194194+ }
195195+ }
196196+197197+ disconnectedCallback() {
198198+ this.abortController?.abort();
199199+ }
200200+201201+ attributeChangedCallback() {
202202+ // Reset to idle if attributes change after an error or success
203203+ if (
204204+ this.state.type === "error" ||
205205+ this.state.type === "subscribed" ||
206206+ this.state.type === "no-publication"
207207+ ) {
208208+ this.state = { type: "idle" };
209209+ }
210210+ this.render();
211211+ }
212212+213213+ get publicationUri() {
214214+ return this.getAttribute("publication-uri") ?? null;
215215+ }
216216+217217+ get callbackUri() {
218218+ return this.getAttribute("callback-uri") ?? "https://sequoia.pub/subscribe";
219219+ }
220220+221221+ get label() {
222222+ return this.getAttribute("label") ?? "Subscribe on Bluesky";
223223+ }
224224+225225+ get hide() {
226226+ const hideAttr = this.getAttribute("hide");
227227+ return hideAttr === "auto";
228228+ }
229229+230230+ async checkPublication() {
231231+ this.abortController?.abort();
232232+ this.abortController = new AbortController();
233233+234234+ try {
235235+ await fetchPublicationUri();
236236+ } catch {
237237+ this.state = { type: "no-publication" };
238238+ this.render();
239239+ }
240240+ }
241241+242242+ async handleClick() {
243243+ if (this.state.type === "loading" || this.state.type === "subscribed") {
244244+ return;
245245+ }
246246+247247+ this.state = { type: "loading" };
248248+ this.render();
249249+250250+ try {
251251+ const publicationUri =
252252+ this.publicationUri ?? (await fetchPublicationUri());
253253+254254+ // POST to the callbackUri (e.g. https://sequoia.pub/subscribe).
255255+ // If the server reports the user isn't authenticated it returns a
256256+ // subscribeUrl for the full-page OAuth + subscription flow.
257257+ const response = await fetch(this.callbackUri, {
258258+ method: "POST",
259259+ headers: { "Content-Type": "application/json" },
260260+ credentials: "include",
261261+ body: JSON.stringify({ publicationUri }),
262262+ });
263263+264264+ const data = await response.json();
265265+266266+ if (response.status === 401 && data.authenticated === false) {
267267+ // Redirect to the hosted subscribe page to complete OAuth
268268+ window.location.href = data.subscribeUrl;
269269+ return;
270270+ }
271271+272272+ if (!response.ok) {
273273+ throw new Error(data.error ?? `HTTP ${response.status}`);
274274+ }
275275+276276+ const { recordUri } = data;
277277+ this.state = { type: "subscribed", recordUri, publicationUri };
278278+ this.render();
279279+280280+ this.dispatchEvent(
281281+ new CustomEvent("sequoia-subscribed", {
282282+ bubbles: true,
283283+ composed: true,
284284+ detail: { publicationUri, recordUri },
285285+ }),
286286+ );
287287+ } catch (error) {
288288+ // Don't overwrite state if we already navigated away
289289+ if (this.state.type !== "loading") return;
290290+291291+ const message =
292292+ error instanceof Error ? error.message : "Failed to subscribe";
293293+ this.state = { type: "error", message };
294294+ this.render();
295295+296296+ this.dispatchEvent(
297297+ new CustomEvent("sequoia-subscribe-error", {
298298+ bubbles: true,
299299+ composed: true,
300300+ detail: { message },
301301+ }),
302302+ );
303303+ }
304304+ }
305305+306306+ render() {
307307+ const { type } = this.state;
308308+309309+ if (type === "no-publication") {
310310+ if (this.hide) {
311311+ this.wrapper.innerHTML = "";
312312+ this.wrapper.style.display = "none";
313313+ }
314314+ return;
315315+ }
316316+317317+ const isLoading = type === "loading";
318318+ const isSubscribed = type === "subscribed";
319319+320320+ const icon = isLoading
321321+ ? `<span class="sequoia-loading-spinner"></span>`
322322+ : isSubscribed
323323+ ? CHECK_ICON
324324+ : BLUESKY_ICON;
325325+326326+ const label = isSubscribed ? "Subscribed" : this.label;
327327+ const buttonClass = [
328328+ "sequoia-subscribe-button",
329329+ isSubscribed ? "sequoia-subscribe-button--success" : "",
330330+ ]
331331+ .filter(Boolean)
332332+ .join(" ");
333333+334334+ const errorHtml =
335335+ type === "error"
336336+ ? `<span class="sequoia-error-message">${escapeHtml(this.state.message)}</span>`
337337+ : "";
338338+339339+ this.wrapper.innerHTML = `
340340+ <button
341341+ class="${buttonClass}"
342342+ type="button"
343343+ part="button"
344344+ ${isLoading || isSubscribed ? "disabled" : ""}
345345+ aria-label="${isSubscribed ? "Subscribed" : this.label}"
346346+ >
347347+ ${icon}
348348+ ${label}
349349+ </button>
350350+ ${errorHtml}
351351+ `;
352352+353353+ if (type !== "subscribed") {
354354+ const btn = this.wrapper.querySelector("button");
355355+ btn?.addEventListener("click", () => this.handleClick());
356356+ }
357357+ }
358358+}
359359+360360+/**
361361+ * Escape HTML special characters (no DOM dependency for SSR).
362362+ * @param {string} text
363363+ * @returns {string}
364364+ */
365365+function escapeHtml(text) {
366366+ return text
367367+ .replace(/&/g, "&")
368368+ .replace(/</g, "<")
369369+ .replace(/>/g, ">")
370370+ .replace(/"/g, """);
371371+}
372372+373373+// Register the custom element
374374+if (typeof customElements !== "undefined") {
375375+ customElements.define("sequoia-subscribe", SequoiaSubscribe);
376376+}
377377+378378+// Export for module usage
379379+export { SequoiaSubscribe };
+2-1
src/config.ts
···3333 toc: true, // Show table of contents (when there is enough page width)
3434 imageViewer: true, // Enable image viewer
3535 copyCode: true, // Enable copy button in code blocks
3636- linkCard: true // Enable link card
3636+ linkCard: true, // Enable link card
3737+ markers: true // Enable highlights on post
3738 }
3839}
+1-1
src/content/about/about.md
···6677augment is a space where I write essays about technology, mostly focused on the open social web and the human internet.
8899-You can subscribe via [email](https://buttondown.com/augment), [rss](https://augment.ink/rss), [atproto](), or [activitypub]().99+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
···11+---
22+title: augment's Atmospheric Home
33+pubDate: 2026-03-10
44+image: /assets/augment-logo.png
55+tags:
66+ - at protocol
77+ - atmosphere
88+ - atproto
99+ - bluesky
1010+ - open social web
1111+ - social media
1212+ - social web
1313+ - blacksky
1414+---
1515+We made it! augment has officially moved over to a self-hosted site, and I'm so excited to tell you all about it.
1616+1717+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.
1818+1919+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==.
2020+2121+### An Atmospheric Blog
2222+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.
2323+2424+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.
2525+* 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
2626+* 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
2727+* 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
2828+* 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/)
2929+3030+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.
3131+3232+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.
3333+3434+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.
3535+3636+==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.
3737+3838+### An Open Foundation
3939+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.
4040+* The site is forked from [Chiri](https://github.com/the3ash/astro-chiri/), an Astro theme that I ever-so-slightly customized to my needs
4141+* 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
4242+* [standard.site](https://standard.site) is a longform standard built by the Atmosphere longform community, kicked off by Offprint, Leaflet, and pckt
4343+* 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
4444+4545+None of this could've been possible without the hard work of the people behind these projects.
4646+4747+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.
4848+4949+### Wherever You Read Your Blogs
5050+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.
5151+5252+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.
5353+5454+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.
5555+5656+Welcome to the new Atmospheric home of augment. I'm so excited to show you more.