Add meta tags for AT URIs#365
Conversation
* WIP: paid memberships — tiers, members-only content, and checkout Adds the paid-membership flow on top of the membership tables: - membersOnlyDelimiter block (lexicon + editor + publish serialization) marking where members-only content begins - server-side gating of post content past the delimiter for non-members (post page, feeds, reader enrichment, tag pages, email broadcasts) - MembersOnlyPaywall and /join tier page with Stripe checkout (startMembershipCheckout), plus membership settings in the publication dashboard - Stripe webhook handling for membership subscriptions (checkout completed, updated, deleted, payment failed) - subscribe flows redirect new subscribers to the join page when a publication has memberships enabled Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Memberships: direct charges on connected accounts + platform wallet Move paid-membership billing from destination charges to direct charges on each publisher's connected Stripe account, with a platform wallet that clones a reader's saved card onto every publisher account. - DB: stripe_wallets, stripe_connected_customers; publication_memberships gains stripe_account_id/cadence/stripe_price_id. - Tiers provision Products/Prices on the publisher's connected account. - Card collection via hosted Checkout (mode:setup) — no Stripe.js/publishable key. Saved card charged server-side off_session; auth-required/decline falls back to Stripe's hosted invoice page. Tier/cadence carried through the redirect so first-time joins auto-complete on return. - New connect-events webhook (listens on connected accounts); membership branches removed from the platform v1 webhook handlers. - Reader self-service at /memberships (cancel/resume/switch tier+cadence, update card); new_member notification; failed-payment email. - Subscribers dashboard tags members (badge + filter). disableMemberships guarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix critical and major billing/correctness issues from paid-memberships review (hyperlink-academy#346) * Fix critical membership billing gaps: webhook reconciliation, identity merge, setup-mode sessions - connect-events webhook can now rebuild a membership row from subscription metadata when no row matches the subscription id (failed join-flow write, row deleted by identity merge), re-fetching the live subscription first so stale event snapshots can't create or resurrect state. Read/write errors on this path now throw so Stripe redelivers. - subscribeToTier sweeps and cancels older duplicate live subscriptions for the same reader+publication after the row upsert (concurrent joins with different tier/cadence bypass the idempotency key). - Identity merge now migrates publication_memberships, stripe_wallets, stripe_connected_customers, and stripe_connected_accounts instead of letting the identities FK cascade delete them while Stripe keeps billing. On collision the active membership wins, target preferred on ties. - Platform checkout.session.completed handler ignores non-subscription-mode sessions instead of throwing: wallet card collection uses setup-mode sessions on the platform account, and each one was a guaranteed 500 retry loop against the Pro billing endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138kgpJVUG36MnMw1tdpmY3 * Fix membership review findings: webhook ordering, tier sync, notifications - Throw on remaining ignored Supabase errors in connect-events handlers (invoice-failed update, payment-failed email reads, notification insert) so the route 500s and Stripe redelivers instead of ACKing lost writes - Re-retrieve the subscription from Stripe before updating a tracked membership row so out-of-order webhook events can't resurrect a canceled membership; invoice-succeeded passes fresh: true to avoid a double fetch - Reorder tier price sync to create new prices and persist them to the DB before archiving replaced ones, and verify a same-amount price is still active before reusing it, so a mid-sync failure can't permanently point a tier at an archived price - Insert new tier rows with active: false so half-provisioned tiers are never offered; also require a monthly price id on the join page - Notify publishers of new members on the inline join path, deduped by membership id via a shared notifyNewMember in src/notifications.ts (the webhook path only fired on inactive-to-active transitions, which the inline flow preempts) - Confirm with Stripe that a lapsed-looking subscription is really inactive before canceling it in subscribeToTier; if it's live, refresh the row from Stripe and return already_member instead - Recompute documents_in_publications.members_only after inflating blob-offloaded pages in sync_document_metadata, closing the gap where gated content was served ungated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138kgpJVUG36MnMw1tdpmY3 * Move notifyNewMember out of the use-server notifications module src/notifications.ts is "use server", so exporting notifyNewMember there made it a wire-callable action anyone could invoke with arbitrary publication/membership ids. It now lives in src/membership.server.ts, a plain server module only importable by server code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138kgpJVUG36MnMw1tdpmY3 --------- Co-authored-by: Claude <noreply@anthropic.com> * Show and manage the viewer's membership in the manage subscription modal The modal now shows the member's tier and price, with Change plan and Cancel (with a confirm screen) swapping the modal's content in place. The tier switcher, cancel confirm, and action buttons are extracted from the memberships & billing page into shared components, and the membership is fetched per publication via an SWR hook gated on the identity's membership rows, with the whole modal behind a loader while it loads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep members-only content out of every public copy of a post The PDS record (and the offloaded pages blob) is public regardless of serve-time gating, so truncate gated posts at the members-only delimiter in every path that writes a document record to a PDS: publish (both record formats), the bskyPostRef re-put, restores, and the record-fixing backfills. The full record now lives only in documents.data; the appview skips re-indexing gated leaflet-managed posts so the truncated firehose copy can't clobber it. Also close the two unauthenticated RPCs that returned full records for any URI (get_document_interactions, get_standard_site_posts), and split the newsletter broadcast into full-body emails for active members (plus owner and confirmed contributors) and preview emails for everyone else. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * render membership thigns * Keep the add-account modal open when launched from the mobile sidebar On mobile the ProfileButton (and the login modal state it owns) is mounted inside the sidebar's Dialog content, so closing the sidebar while opening the add-account modal unmounted the modal immediately. Leave the sidebar open underneath, like the other modals in this menu. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Serve members-only images from publish-owned storage instead of the PDS Images past the members-only delimiter were uploaded as PDS blobs, but the truncated public record doesn't reference them, leaving them unreferenced (subject to PDS garbage collection) and publicly fetchable besides. Now publish serialization threads a membersOnly flag through to the upload hooks, and gated images skip the PDS entirely: they're copied into a document-scoped folder (gated-documents/<did>/<rkey>/) so they survive draft-asset deletion, deduplicated by the draft's uuid filename so republishes reuse existing copies, and stale copies are garbage collected after each successful publish. The record carries them as http(s) $link blobrefs, which blobRefToSrc already passes through on every render path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove duplicated src/membership import Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * add admin entitlement ui * Cancel member subscriptions before deleting a publication deletePublication hard-deleted the publications row, cascading away the membership rows while leaving members' Stripe subscriptions live on the publisher's connected account — billing forever with no record and no in-app way to cancel. Cancel every member subscription first, and abort the delete if any can't be confirmed canceled. Also guard reconcileUntrackedSubscription against a deleted publication: renewal events for a residual orphan previously hit the publication FK violation, 500ing the webhook and making Stripe retry indefinitely; now they log for manual cancellation instead. Also fix handleMembershipInvoiceFailed discarding the subscription status it just re-fetched: it wrote a literal past_due, so a replayed or out-of-order invoice.payment_failed arriving after the invoice recovered would re-gate an active, paying member and send them a dunning email. Write sub.status instead, and skip the payment-failed email when the subscription is already active again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add min-w-0 to the list item content wrapper so flex items (e.g. bsky embeds) can shrink below their intrinsic width instead of overflowing the page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a URL/HTML toggle to the embed block: pasted HTML is stored as an embed/html fact and rendered via the iframe srcdoc attribute, sandboxed without allow-same-origin so it can't script against the app. Published via a new optional html field on pub.leaflet.blocks.iframe (url now optional too). The empty block is now resizable via the same height drag handle as filled embeds, and the header offers aspect ratio presets (Fixed, 4:3, 1:1, 4:1) that persist through publish. Explicitly chosen sizes are no longer overwritten by embed metadata on submit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@ChrisShank is attempting to deploy a commit to the Hyperlink Team on Vercel. A member of the Team first needs to authorize it. |
|
Amazing!! Yep I think a good idea to keep the link tags, unless they conflict for some reason? |
|
@jaredpereira Added them back! There are no conflicts, but from some of the research I did AT URIs in standardized link tags could affect SEO, specifically for |
|
There's a lot of other changes here, I think something got wierd with the git history. I accidentally commited some scratch files to the repo earlier and had to remove them from the history, so maybe rebasing your commits onto our main here could fix them? Sorry about the trouble, happy to do it as well if you run into anything more! |
This PR implements the community proposal for mapping web pages back AT URIs. It removes
link[rel=alternate]tags, but keeps thelink[rel=standard.site.document]tags. If there are worries that this is a breaking change, we can keep<link>for the time being.As for standard.site using the new proposal I'm thinking that it would look like this. I think the first step is to get the big 3 to start publishing these tags.