a tool for shared writing and social publishing

Dynamic static split (#371) master

* Split the app into cacheable (published) and dynamic (identity) route groups The shared (app) layout awaited headers() and the 15-relation identity query on every request, forcing all ~45 routes dynamic. Published surfaces (publication homes, posts, archives, /p posts, subscribe pages) now live in app/(app)/(published), read no request state, and serve via on-demand ISR (revalidate 300s, expireTime-capped SWR window); dashboard/editor/account surfaces live in app/(app)/(identity) and keep per-request server identity, which is what preserves first-frame theming. Identity on published pages is a client island: a slim getViewerIdentity action fetched on mount, gated by a non-httpOnly leaflet_session marker cookie set/cleared beside the auth cookies (with middleware backfill for existing sessions), so anonymous readers and crawlers trigger zero identity queries. Dashboard and published providers use separate SWR keys, seeded across group navigations so a known-logged-in viewer never flashes logged-out. Identity-dependent controls keep stable geometry via ReservedGeometry (invisible inert copy pins the box) and overlay-positioned owner affordances. Members-only posts always render the anonymous truncated variant; entitled viewers unlock the tail through a server-checked action with the pages and gated flag flipping atomically in PostDataProvider. The document payload is now an explicit projection — the previous publications(*) spread leaked draft_leaflet (a permission-token id honored by /[leaflet_id] without auth) and subscriber DIDs into public HTML. Also: - Query-string reads in the post tree go through useClientSearchParams (SSR stays param-free; deep links apply post-hydration) because bare useSearchParams strips the subtree from static HTML. - revalidatePath calls were silent no-ops (ISR tags include the route group); publish/delete/theme-change now invalidate concrete rkey- and name-form paths via revalidatePublicationPaths, and post publish invalidates at all (it previously never did). - generateMetadata double-fetches collapsed with React.cache (including 2x get_leaflet_data + 2x get_facts per editor request); OG images no longer read headers() and actually cache for 24h. - Dashboard: per-segment loading.tsx inside the persistent shell, eager prefetch on nav links, Replicache client no longer torn down on every RSC re-render, nav-state cookie POSTs deduped. - Misses return real 404s instead of cacheable 200s; pub-page errors rethrow instead of rendering; scripts/check-published-purity.mjs guards the (published) invariants in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix identity-group navigation transitions and cross-nav UI state - Give the (identity) and (home-pages) layouts a synchronous shell with their request-coupled awaits behind Suspense, so navigations into the group commit a loading fallback immediately (~50ms) instead of freezing on the previous page until the whole dynamic tree renders - Cache the members-only unlock in the SWR module cache per (post, viewer) so returning to an unlocked post doesn't re-flash the paywall, and show the pending state from the first frame while the check resolves - Keep the mobile sidebar drawer open when tapping navigation links so it persists across navigations; only non-link actions dismiss it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Actually keep the mobile sidebar open on nav-link taps The keep-open check tested the clicked element's tagName, but the sidebar nav links render a button inside the anchor (SpeedyLink > ActionButton), so closest("a, button") resolved to the inner button and the drawer still closed on every navigation. Match any interactive element inside an anchor instead, and drop the profile menu's explicit sidebar closes on its navigation links (logout keeps its close — it hard-redirects). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * trim getIdentityData everywhere * combine home query * tweak membership content box * allow authing into vercel previews * allow sign in to preview domains * Fix correctness issues found reviewing the dynamic/static split Keep the draft-leaflet permission token out of published pages. It is a bearer capability that /[leaflet_id] redeems with no identity check, and the projection shipped it to every reader of what is now one shared CDN entry. The owner's edit affordances fetch it from getPostEditLink, which re-derives ownership server-side, instead of reading it off the payload. Match the leaflet_session marker's lifetime to the cookie it mirrors — external_auth_token is session-scoped, so a year-long marker outlived it and made every published page view fetch an identity that was gone. Seed identityPending from the session marker before paint, so entitled members stop seeing one logged-out frame of the paywall and the subscribe button. Teach the published-purity check to walk the import graph (stopping at "use server"/"use client", where nothing runs during the render) and to assert public pages still declare both revalidate and generateStaticParams. Restore the two columns getIdentityData's trim dropped, which had silently regressed home's Copy Post Link to the /p/ permalink, and pin them with a type-level assertion the cast was hiding. Seed the subscribe email field once identity resolves — it snapshotted a logged-out value and left an input that could be neither typed into nor submitted — and refresh identity after an atproto one-click subscribe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Simplify duplication left over from the dynamic/static split - Share revalidate-path subpath logic between publishToPublication and deletePost via revalidatePostPaths - Share the identity embed-column fragments between getIdentityData and getViewerIdentity so they can't drift apart - Collapse the publication home page's two overlapping queries (fetchPublicationRecordForMetadata + fetchPublicationForPage) into one - Apply the transition-shell sync+Suspense pattern to the reader and profile layouts, which still blocked navigation on their own request-coupled awaits Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * simplify some comments --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>


+23931 -21193
530 changed files