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

feat: Add subscription component #25

merged opened by heaths.dev targeting main from heaths.dev/sequoia: issue16

Resolves #16

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:tg3tb5wukiml4xmxml6qm637/sh.tangled.repo.pull/3mfe5ptlpci22
+192
Interdiff #2 โ†’ #3
bun.lock

This file has not been changed.

docs/package.json

This file has not been changed.

docs/src/index.ts

This file has not been changed.

docs/src/lib/oauth-client.ts

This file has not been changed.

docs/src/lib/path-redirect.ts

This file has not been changed.

docs/src/lib/session.ts

This file has not been changed.

docs/src/routes/auth.ts

This file has not been changed.

docs/src/routes/subscribe.ts

This file has not been changed.

docs/wrangler.toml

This file has not been changed.

packages/cli/src/commands/add.ts

This file has not been changed.

packages/cli/src/commands/publish.ts

This file has not been changed.

packages/cli/src/components/sequoia-subscribe.js

This file has not been changed.

packages/cli/src/lib/atproto.ts

This file has not been changed.

packages/cli/src/lib/types.ts

This file has not been changed.

+191
docs/docs/pages/subscribe.mdx
··· 1 + # Subscribe 2 + 3 + Sequoia provides a subscribe button web component that lets your readers subscribe to your publication directly from your site using their Bluesky account. 4 + 5 + ## Setup 6 + 7 + Run the following command in your project to install the subscribe web component. It will ask you where you would like to store the component file. 8 + 9 + ```bash [Terminal] 10 + sequoia add sequoia-subscribe 11 + ``` 12 + 13 + The component will look for your publication AT URI from your site's `/.well-known/site.standard.publication` endpoint automatically, so no additional configuration is required for most setups. 14 + 15 + ## Usage 16 + 17 + Since `sequoia-subscribe` is a standard Web Component, it works with any framework. Choose your setup below: 18 + 19 + :::code-group 20 + 21 + ```html [HTML] 22 + <body> 23 + <h1>My Publication</h1> 24 + <!--Content--> 25 + 26 + <sequoia-subscribe></sequoia-subscribe> 27 + <script type="module" src="./src/components/sequoia-subscribe.js"></script> 28 + </body> 29 + ``` 30 + 31 + ```tsx [React] 32 + // Import the component (registers the custom element) 33 + import './components/sequoia-subscribe.js'; 34 + 35 + function HomePage() { 36 + return ( 37 + <main> 38 + <h1>My Publication</h1> 39 + {/* Content */} 40 + <sequoia-subscribe /> 41 + </main> 42 + ); 43 + } 44 + ``` 45 + 46 + ```vue [Vue] 47 + <script setup> 48 + import './components/sequoia-subscribe.js'; 49 + </script> 50 + 51 + <template> 52 + <main> 53 + <h1>My Publication</h1> 54 + <!-- Content --> 55 + <sequoia-subscribe /> 56 + </main> 57 + </template> 58 + ``` 59 + 60 + ```svelte [Svelte] 61 + <script> 62 + import './components/sequoia-subscribe.js'; 63 + </script> 64 + 65 + <main> 66 + <h1>My Publication</h1> 67 + <!-- Content --> 68 + <sequoia-subscribe /> 69 + </main> 70 + ``` 71 + 72 + ```astro [Astro] 73 + <main> 74 + <h1>My Publication</h1> 75 + <!-- Content --> 76 + <sequoia-subscribe /> 77 + <script> 78 + import './components/sequoia-subscribe.js'; 79 + </script> 80 + </main> 81 + ``` 82 + 83 + ::: 84 + 85 + ### TypeScript Support 86 + 87 + If you're using TypeScript with React, add this type declaration to avoid JSX errors: 88 + 89 + ```ts [custom-elements.d.ts] 90 + declare namespace JSX { 91 + interface IntrinsicElements { 92 + 'sequoia-subscribe': React.DetailedHTMLProps< 93 + React.HTMLAttributes<HTMLElement> & { 94 + 'publication-uri'?: string; 95 + 'callback-uri'?: string; 96 + label?: string; 97 + hide?: string; 98 + }, 99 + HTMLElement 100 + >; 101 + } 102 + } 103 + ``` 104 + 105 + ### Vue Configuration 106 + 107 + For Vue, you may need to configure the compiler to recognize custom elements: 108 + 109 + ```ts [vite.config.ts] 110 + export default defineConfig({ 111 + plugins: [ 112 + vue({ 113 + template: { 114 + compilerOptions: { 115 + isCustomElement: (tag) => tag === 'sequoia-subscribe' 116 + } 117 + } 118 + }) 119 + ] 120 + }); 121 + ``` 122 + 123 + ## Configuration 124 + 125 + The subscribe web component has several configuration options available. 126 + 127 + ### Attributes 128 + 129 + The `<sequoia-subscribe>` component accepts the following attributes: 130 + 131 + | Attribute | Type | Default | Description | 132 + |-----------|------|---------|-------------| 133 + | `publication-uri` | `string` | - | AT Protocol URI for the publication. Optional if a `/.well-known/site.standard.publication` endpoint exists on the host site. | 134 + | `callback-uri` | `string` | `https://sequoia.pub/subscribe` | Redirect URI used for the OAuth authentication flow. | 135 + | `label` | `string` | `Subscribe on Bluesky` | Button label text. | 136 + | `hide` | `string` | - | Set to `"auto"` to hide the component if no publication URI is detected. | 137 + 138 + ```html 139 + <!-- Use attributes for explicit control --> 140 + <sequoia-subscribe 141 + publication-uri="at://did:plc:example/site.standard.publication/abc123" 142 + label="Follow on Bluesky"> 143 + </sequoia-subscribe> 144 + ``` 145 + 146 + ### Events 147 + 148 + The component dispatches custom events you can listen to: 149 + 150 + | Event | Description | `detail` | 151 + |-------|-------------|----------| 152 + | `sequoia-subscribed` | Fired when the subscription is created successfully. | `{ publicationUri: string, recordUri: string }` | 153 + | `sequoia-subscribe-error` | Fired when the subscription fails. | `{ message: string }` | 154 + 155 + ```js 156 + const btn = document.querySelector('sequoia-subscribe'); 157 + 158 + btn.addEventListener('sequoia-subscribed', (e) => { 159 + console.log('Subscribed!', e.detail.recordUri); 160 + }); 161 + 162 + btn.addEventListener('sequoia-subscribe-error', (e) => { 163 + console.error('Subscription failed:', e.detail.message); 164 + }); 165 + ``` 166 + 167 + ### Styling 168 + 169 + The component uses CSS custom properties for theming. Set these in your `:root` or parent element to customize the appearance: 170 + 171 + | CSS Property | Default | Description | 172 + |--------------|---------|-------------| 173 + | `--sequoia-fg-color` | `#1f2937` | Text color | 174 + | `--sequoia-bg-color` | `#ffffff` | Background color | 175 + | `--sequoia-border-color` | `#e5e7eb` | Border color | 176 + | `--sequoia-accent-color` | `#2563eb` | Button background color | 177 + | `--sequoia-secondary-color` | `#6b7280` | Secondary text color | 178 + | `--sequoia-border-radius` | `8px` | Border radius for the button | 179 + 180 + ### Example: Match Site Theme 181 + 182 + ```css 183 + :root { 184 + --sequoia-accent-color: #3A5A40; 185 + --sequoia-border-radius: 6px; 186 + --sequoia-bg-color: #F5F3EF; 187 + --sequoia-fg-color: #2C2C2C; 188 + --sequoia-border-color: #D5D1C8; 189 + --sequoia-secondary-color: #8B7355; 190 + } 191 + ```
+1
docs/vocs.config.ts
··· 34 34 { text: "Setup", link: "/setup" }, 35 35 { text: "Publishing", link: "/publishing" }, 36 36 { text: "Comments", link: "/comments" }, 37 + { text: "Subscribe", link: "/subscribe" }, 37 38 { text: "Verifying", link: "/verifying" }, 38 39 { text: "Workflows", link: "/workflows" }, 39 40 ],

History

4 rounds 14 comments
sign up or login to add to the discussion
5 commits
expand
feat: Add subscription component
Add subscription support
Add cors support
Subscribe UI improvements
Add subscribe section to documentation and update navigation links
expand 2 comments

Excellent; thank you!! I'll make some follow up changes with the fetch-patch for the worker. Really appreciate your help here!

oop nvm, see its there now; we're good!

pull request successfully merged
4 commits
expand
feat: Add subscription component
Add subscription support
Add cors support
Subscribe UI improvements
expand 1 comment

I wasn't able to test e2e. I can get to the /subscribe page and authenticate, but with the static and API sites running on different ports I don't get a proper callback.

2 commits
expand
feat: Add subscription component
Add subscription support
expand 11 comments

@stevedylan.dev this is closed, but I'm having trouble verifying it e2e. I swapped out the CLIENT_URL variable for my local instance, but seems I need both the static site and wrangler site (on separate ports) running simultaneously and I'm not sure the flow is working right between the two as it might for Cloudflare. Is this something you can test easily, or maybe have some pointers? Does Cloudflare somehow proxy the calls to seem like a single site?

All that said, the flow seems to almost work right. I had to disable CORS for localhost on my machine but that should just be because it's localhost. I explicitly set the callback-uri in my SSG to test this all out. The rendered pages are somewhat themed. Not sure how vocs is setting the class on the HTML root element, but I have a script doing it here.

The auto-hide functionality also works if the publication-uri isn't set or isn't discoverable from /.well-known.

I suppose I should add a help topic before calling this "done", too. Something short and sweet like the Comments topic.

I suppose I should add a help topic before calling this "done", too. Something short and sweet like the Comments topic.

@heaths.dev Awesome!! Yeah I can definitely test this later. I think if you run bun dev:api inside the docs folder it should spin up the actual API and render the site as the same time, so that should make it easier to test!

We also probably need to update the CORS on the index.ts file too, something like seen here: https://hono.dev/docs/middleware/builtin/cors

Whew, ok had to do some deep testing and deployment of code that still got me pretty stuck. Here's where we're at:

  • We need to add these cors settings to the index.ts file
import { cors } from "hono/cors";
// Other imports and initial hono app
app.use(
	"/subscribe/*",
	cors({
		origin: (origin) => origin,
		credentials: true,
	}),
);
app.use(
	"/subscribe",
	cors({
		origin: (origin) => origin,
		credentials: true,
	}),
);
  • For some reason the OAuth client really doesn't play well with cloudflare workers. Here's an example error:
GET https://sequoia.pub/oauth/login?handle=stevedylan.dev - Ok @ 2/24/2026, 8:11:56 PM
  (error) Identity resolution failed: DidError did-unknown-error (did:plc:ia2zdnhjaokf5lazhxrmj6eu): Invalid redirect value, must be one of "follow" or "manual" ("error" won't be implemented since it does not make sense at the edge; use "manual" and check the response status code).
  (error) Login error: Error: Failed to resolve identity: stevedylan.dev

I know with the CLI we use the @atproto/oauth-client-node so maybe that's the answer. Will see if I can take another crack at it later.

Figured it out!! What a pain in the ass lol. Ok here's what we need to do:

  • Create a new file docs/src/lib/path-redirect.ts with these contents and then import it to docs/src/index.ts like so import "./lib/patch-redirect";. This will solve the weird cloudflare worker issues with the atproto oauth client library and how it handles redirects.

  • Update all instances of scope: "atproto transition:generic" to scope: "atproto site.standard.graph.subscription"

  • The cors updates seen in the previous comment

I made these changes locally and deployed it so you can try it out from localhost and it should work! We just need to get the actual code committed and merged.

Also on the final screen where it shows the user that we created the records, could be nice to link to a pds.ls URL like so

https://pds.ls/at://did:plc:ia2zdnhjaokf5lazhxrmj6eu/site.standard.graph.subscription/3mfnrenwnps2h

Hold off on changing the scopes; started having weird issues with it so reverted to what you already have

Ok tested some more with scopes and I'm not sure what I was doing wrong before, perhaps an old session, but I confirmed it's working as expected. Just need to update the following lines:

docs/src/routes/auth.ts:30,47 docs/src/lib/oauth-client.ts:22

scope: "atproto repo:site.standard.graph.subscription"

Hey, sorry for not responding earlier and thanks so much for testing this with Cloudflare! I was building the docs site okay. The problem I was running into was that the static site was served from a different port than the API, and crossing between the two was having additional CORS issues than just the ones I expected tested against localhost that I temporarily worked around.

Since you already have the changes, do you just want to push them to this PR since you can actually test them? Given our previous thread, I don't mind that at all - it's collaboration! :) That was different than the other PR.

That said, not sure if you're online right now so I'll take a look through your changes and incorporate what I can. I'll even try to test but, like I said, without testing in an actual test environment on Cloudflare (or something like it), not sure how realistic testing would be.

1 commit
expand
feat: Add subscription component
expand 0 comments