···11+# Switch Bluesky view-links to mu.social + π icon
22+33+**Date:** 2026-06-19
44+**Status:** Approved (design)
55+66+## Problem
77+88+SkyPress sends readers to `bsky.app` to view profiles, posts, and hashtags, and
99+marks those links with a Bluesky butterfly icon. We want those *view* links to
1010+point at `mu.social` (an alternative AT Protocol AppView, e.g.
1111+`https://mu.social/profile/skypress.blog`) and to mark them with the `π` emoji
1212+instead of the butterfly.
1313+1414+This only concerns where we send a reader to *view* something. SkyPress still
1515+publishes to the Bluesky/atproto network exactly as before.
1616+1717+## Scope
1818+1919+### In scope β view-links move to `mu.social`
2020+2121+| Place | File | Today |
2222+|---|---|---|
2323+| Author profile `@handle` link | `src/pages/[author]/index.astro` | `bsky.app/profile/<handle>` |
2424+| Bio `@mention` links | `src/lib/reader/rich-text.ts` | `bsky.app/profile/<handle>` |
2525+| Bio `#hashtag` links | `src/lib/reader/rich-text.ts` | `bsky.app/hashtag/<tag>` |
2626+| Editor mention autocomplete (stored in content) | `src/lib/editor/mention-autocompleter.ts` | `bsky.app/profile/<handle>` |
2727+| Post "view" links | `src/lib/social/records.ts` (`bskyPostWebUrl`) | `bsky.app/profile/<did>/post/<rkey>` |
2828+| "Create an account" link | `src/components/SignInPanel.tsx` | `https://bsky.app` |
2929+3030+`mu.social` is a Bluesky AppView client and uses the same path scheme
3131+(`/profile/<id>`, `/profile/<did>/post/<rkey>`, `/hashtag/<tag>`). The profile
3232+path is confirmed live; the post/hashtag paths are the standard AppView scheme
3333+and will be verified with a real link during the smoke test.
3434+3535+### Out of scope β deliberately unchanged
3636+3737+- `public.api.bsky.app` API calls (actor lookup in `src/lib/landing/actor-lookup.ts`,
3838+ interaction counts in `src/lib/social/interactions.ts`). mu.social does not serve
3939+ that data API.
4040+- Network/protocol descriptions: "publishing creates a public Bluesky post", the
4141+ `app.bsky.feed.post` lexicon record on `src/pages/lexicon.astro`, unpublish
4242+ warnings, and publish-disclosure copy. These describe the real atproto/Bluesky
4343+ network and stay accurate.
4444+- **Existing published articles** keep the `bsky.app` mention links already stored
4545+ in their document HTML. We do not rewrite saved content; only newly authored
4646+ mentions get `mu.social` links.
4747+4848+## Architecture
4949+5050+Introduce a single dependency-free helper module so the host lives in one place
5151+and is safe to import from the reader path (no `@wordpress/*`):
5252+5353+```ts
5454+// src/lib/social/atmosphere-url.ts
5555+export const ATMOSPHERE_WEB_BASE = 'https://mu.social';
5656+export function atmosphereProfileUrl( identifier: string ): string; // handle or DID
5757+export function atmospherePostWebUrl( atUri: string ): string; // at:// β web URL
5858+export function atmosphereHashtagUrl( tag: string ): string;
5959+```
6060+6161+- `atmospherePostWebUrl` absorbs the existing `bskyPostWebUrl` logic (parses
6262+ `at://<did>/app.bsky.feed.post/<rkey>`, falls back to `ATMOSPHERE_WEB_BASE` on an
6363+ unparseable URI). `bskyPostWebUrl` is removed and its one caller
6464+ (`src/components/PostActions.tsx`) updated.
6565+- All six call sites switch to these helpers.
6666+6767+### Critical coupling
6868+6969+`src/lib/publish/mentions.ts` (`PROFILE_RE`) parses stored mention `href`s to
7070+decide who gets cc'd on the companion Bluesky post. Because the editor will now
7171+insert `mu.social` hrefs, this regex MUST match both hosts, or mentions silently
7272+stop being cc'd. Widen it to accept `bsky.app` **and** `mu.social` (this also keeps
7373+already-saved drafts working).
7474+7575+## Wording
7676+7777+Link labels / aria-labels that say "Bluesky" become **"the ATmosphere"**:
7878+7979+- `src/components/PostActions.tsx`: "View this post on Bluesky" β "View this post on
8080+ the ATmosphere"; "View on Bluesky" β "View on the ATmosphere".
8181+- `src/pages/[author]/index.astro`: aria-label "View @handle on Bluesky" β "View
8282+ @handle on the ATmosphere".
8383+- `src/components/SignInPanel.tsx`: "Create one on Bluesky" and its aria-label β
8484+ "β¦on the ATmosphere".
8585+8686+Disclosure / network text stays "Bluesky" (it describes the network, not a link):
8787+"Likes, reposts⦠happen on Bluesky", "creates a public Bluesky post", unpublish
8888+warnings, the lexicon page.
8989+9090+## The π icon
9191+9292+- **Author profile** (`src/pages/[author]/index.astro`): replace the inline
9393+ butterfly SVG with the `π` emoji as text.
9494+- **Article mentions** (`src/pages/[author]/[slug]/[rkey].astro`): replace the CSS
9595+ `mask-image` butterfly on `a.skypress-mention::before` with `content: "π"`. The
9696+ glyph becomes multicolor (no longer tinted by the publication theme); the hover
9797+ slide-in transform is preserved.
9898+9999+## Testing (TDD)
100100+101101+Write/extend failing tests first:
102102+103103+- New `src/lib/social/atmosphere-url.test.ts` β profile (handle + DID), post
104104+ (`at://` β web URL, plus fallback), hashtag.
105105+- Update `src/lib/reader/rich-text` tests β profile and hashtag URLs now `mu.social`.
106106+- Update `src/lib/publish/mentions` tests β `PROFILE_RE` matches both `bsky.app`
107107+ and `mu.social` hrefs.
108108+- Update any post-URL test currently asserting `bskyPostWebUrl`/`bsky.app`.
109109+110110+Run `npm run test` and `npm run check`.
111111+112112+## Decision record
113113+114114+Record the canonical view-host change (bsky.app β mu.social, and the
115115+"the ATmosphere" link wording) as `docs/decisions/0022-atmosphere-view-host.md`.