From af07421b2215e6ec7e1164c4fe5a2ff9bb08feb5 Mon Sep 17 00:00:00 2001 From: zlshames Date: Fri, 18 Sep 2026 16:13:24 -0400 Subject: [PATCH 1/2] Add a compatibility page driven by the capability catalogue What the Private API can do depends on the macOS version on the server Mac, and the only place that was written down was a pair of hand-maintained HTML tables inside a single FAQ answer -- behind an accordion, inside a category, three clicks from anywhere. It is the most decision-relevant content on the site and was the least visible. /compatibility lists all 23 capabilities, grouped by category, each with its macOS floor and a one-line summary, and reuses the macOS picker: choose a release and the page highlights what it supports. 16 of 23 on Sonoma, 20 on Sequoia, 23 on Tahoe. Data is copied from the server's CapabilityCatalog.swift -- ids, titles, summaries, macOS floors and categories -- so the page and the server describe the same thing rather than drifting. Deliberately no subscription tier on this page. It answers "does my Mac support this", which is a different question from what a plan includes, and Pro is not announced. Adding a tier badge later is additive, and nothing here links to or mentions /pricing, so that page stays undiscoverable -- asserted in the build. The picker itself is now a shared component. The pricing page was the only user and had the markup and ~40 lines of script inline; both pages now use one copy, so the cumulative rule cannot diverge between them. Verified the pricing page is unchanged by the refactor: 10 cards, 5/7/10 across the releases, still noindex, prices intact. Linked from the header and footer, since being findable is the point. Co-Authored-By: Claude Opus 5 --- scripts/verify-build.mjs | 1 + src/components/MacOSPicker.astro | 110 ++++++++++++++ src/data/capabilities.ts | 223 ++++++++++++++++++++++++++++ src/data/site.ts | 2 + src/pages/compatibility/index.astro | 101 +++++++++++++ src/pages/pricing/index.astro | 104 ++----------- 6 files changed, 447 insertions(+), 94 deletions(-) create mode 100644 src/components/MacOSPicker.astro create mode 100644 src/data/capabilities.ts create mode 100644 src/pages/compatibility/index.astro diff --git a/scripts/verify-build.mjs b/scripts/verify-build.mjs index 402eb71..bfcf6b2 100644 --- a/scripts/verify-build.mjs +++ b/scripts/verify-build.mjs @@ -29,6 +29,7 @@ const REQUIRED_PAGES = [ 'downloads/server/index.html', 'install/index.html', 'faq/index.html', + 'compatibility/index.html', 'donate/index.html', // Not linked from anywhere, but Creem validates this URL -- so a build // that stops emitting it should fail rather than pass quietly. diff --git a/src/components/MacOSPicker.astro b/src/components/MacOSPicker.astro new file mode 100644 index 0000000..b970b78 --- /dev/null +++ b/src/components/MacOSPicker.astro @@ -0,0 +1,110 @@ +--- +import { MACOS_RELEASES } from '@data/pricing'; + +/* + * A macOS release picker that highlights which features a given release can + * run. Shared by the pricing and compatibility pages so the two cannot drift. + * + * It operates on any `[data-feature][data-macos]` elements on the page: it sets + * data-state="included"/"excluded" on each and fills its `[data-verdict]` line. + * Styling lives with the cards, not here. + * + * Releases are cumulative -- a release runs everything introduced at or below + * its version -- which is a `<=` on the major version and nothing more. + */ +interface Props { + /** Text before the select. */ + label: string; + /** Noun for the count line, e.g. "Pro features" or "features". */ + noun: string; +} +const { label, noun } = Astro.props; + +// Default to the newest release. Derived from the data, so adding macOS 27 +// moves the default automatically. +const newest = MACOS_RELEASES.reduce((a, b) => (b.version > a.version ? b : a)); +--- + +
+ + + {/* + appearance-none plus our own chevron: the native arrow sits inside the right + padding, and a rounded-full pill's corner radius curves in over it. + */} +
+ + +
+ +

+ +

+
+ + diff --git a/src/data/capabilities.ts b/src/data/capabilities.ts new file mode 100644 index 0000000..db9f503 --- /dev/null +++ b/src/data/capabilities.ts @@ -0,0 +1,223 @@ +/** The Private API capability catalogue, for the compatibility page. + * + * Copied from the server's `Sources/BBPrivateAPICatalog/CapabilityCatalog.swift` + * -- id, title, summary, macOS floor and category all come from there, so the + * page and the server describe the same thing. If a capability is added or its + * macOS floor moves, update it here too. + * + * Deliberately no subscription tier: this page answers "does my Mac support + * it", which is a separate question from what a plan includes, and Pro is not + * announced yet. Adding a tier badge later is additive. + */ + +export interface Capability { + id: string; + title: string; + summary: string; + /** Minimum macOS major version on the server Mac. */ + macos: number; + category: CapabilityCategory; +} + +export type CapabilityCategory = + | 'messages' + | 'reactions' + | 'organising' + | 'faceTime' + | 'findMy'; + +/** Section headings, in the order they appear on the page. */ +export const CAPABILITY_CATEGORIES: readonly { id: CapabilityCategory; label: string }[] = [ + { id: 'messages', label: 'Messaging' }, + { id: 'reactions', label: 'Reactions and stickers' }, + { id: 'organising', label: 'Organising conversations' }, + { id: 'faceTime', label: 'FaceTime' }, + { id: 'findMy', label: 'Find My' }, +] as const; + +export const CAPABILITIES: readonly Capability[] = [ + { + id: 'rich-sending', + title: 'Improved message sending', + summary: + 'Send with a subject line, and more reliably than the scripting interface manages.', + macos: 14, + category: 'messages', + }, + { + id: 'message-effects', + title: 'Bubble and screen effects', + summary: + 'Send with slam, loud, gentle, invisible ink, confetti, fireworks and the rest.', + macos: 14, + category: 'messages', + }, + { + id: 'replies', + title: 'Threaded replies', + summary: + 'Reply to one specific message, so the conversation keeps the thread.', + macos: 14, + category: 'messages', + }, + { + id: 'mentions', + title: 'Mentions', + summary: + 'Mention someone by name in a group so they are notified even on Do Not Disturb.', + macos: 14, + category: 'messages', + }, + { + id: 'edit-message', + title: 'Edit a sent message', + summary: + 'Change the wording of a message you already sent, as Messages does.', + macos: 14, + category: 'messages', + }, + { + id: 'unsend-message', + title: 'Unsend a message', + summary: + 'Take back a message so it disappears for everyone in the conversation.', + macos: 14, + category: 'messages', + }, + { + id: 'typing-indicators', + title: 'Typing indicators', + summary: + 'See when someone is typing to you, and show them when you are.', + macos: 14, + category: 'messages', + }, + { + id: 'read-state', + title: 'Mark as read or unread', + summary: + 'Clear a conversation\'s badge, or put it back, from a client.', + macos: 14, + category: 'messages', + }, + { + id: 'send-later', + title: 'Send Later', + summary: + 'Hand a message to iMessage to deliver later. Apple sends it whether or not this Mac is running, unlike the server\'s own scheduled messages.', + macos: 15, + category: 'messages', + }, + { + id: 'text-formatting', + title: 'Text formatting', + summary: + 'Bold, italics, underline and strikethrough inside a message.', + macos: 15, + category: 'messages', + }, + { + id: 'polls', + title: 'Polls', + summary: + 'Create a poll in a group and collect votes from everyone in it.', + macos: 26, + category: 'messages', + }, + { + id: 'tapbacks', + title: 'Reactions', + summary: + 'Love, like, dislike, laugh, emphasise and question, on any message.', + macos: 14, + category: 'reactions', + }, + { + id: 'stickers', + title: 'Stickers', + summary: + 'Send a sticker, and place it on a message the way Messages does.', + macos: 14, + category: 'reactions', + }, + { + id: 'emoji-reactions', + title: 'Emoji reactions', + summary: + 'React with any emoji, not just the six built-in tapbacks.', + macos: 15, + category: 'reactions', + }, + { + id: 'sticker-reactions', + title: 'Sticker reactions', + summary: + 'Send a sticker as a reaction to a message, the way a tapback attaches to it.', + macos: 15, + category: 'reactions', + }, + { + id: 'group-management', + title: 'Managing groups', + summary: + 'Rename a group, add or remove people, change its photo, and leave it.', + macos: 14, + category: 'organising', + }, + { + id: 'pinning', + title: 'Pinning conversations', + summary: + 'Pin a conversation to the top, in the same order as your other devices.', + macos: 14, + category: 'organising', + }, + { + id: 'muting', + title: 'Muting conversations', + summary: + 'Silence a conversation, permanently or until a time you choose.', + macos: 14, + category: 'organising', + }, + { + id: 'junk-reporting', + title: 'Spam and junk', + summary: + 'Report a conversation as junk, mark it as spam, and move it out of Unknown Senders.', + macos: 14, + category: 'organising', + }, + { + id: 'chat-backgrounds', + title: 'Conversation backgrounds', + summary: + 'Set a background image for a conversation, shared with everyone in it.', + macos: 26, + category: 'organising', + }, + { + id: 'screen-unknown-senders', + title: 'Screen unknown senders', + summary: + 'See whether a sender is known, and accept one into your contacts.', + macos: 26, + category: 'organising', + }, + { + id: 'facetime', + title: 'FaceTime from a client', + summary: + 'Answer or end a call, and create a link someone can join from anywhere.', + macos: 14, + category: 'faceTime', + }, + { + id: 'find-my', + title: 'Find My', + summary: + 'See where your friends and devices are.', + macos: 14, + category: 'findMy', + }, +] as const; diff --git a/src/data/site.ts b/src/data/site.ts index dce73ea..f99e599 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -43,6 +43,7 @@ export interface NavItem { export const NAV_LINKS: readonly NavItem[] = [ { href: '/downloads/', label: 'Downloads' }, { href: '/install/', label: 'Install' }, + { href: '/compatibility/', label: 'Compatibility' }, { href: '/faq/', label: 'FAQ' }, { href: '/donate/', label: 'Donate' }, // Absolute on purpose: /web is a *separate* Pages deploy @@ -54,6 +55,7 @@ export const FOOTER_LINKS: readonly NavItem[] = [ { href: '/', label: 'Home' }, { href: '/downloads/', label: 'Downloads' }, { href: '/install/', label: 'Install' }, + { href: '/compatibility/', label: 'Compatibility' }, { href: '/faq/', label: 'FAQ' }, { href: LINKS.webApp, label: 'Web App', external: true }, { href: '/donate/', label: 'Donate' }, diff --git a/src/pages/compatibility/index.astro b/src/pages/compatibility/index.astro new file mode 100644 index 0000000..2e08649 --- /dev/null +++ b/src/pages/compatibility/index.astro @@ -0,0 +1,101 @@ +--- +import BaseLayout from '@layouts/BaseLayout.astro'; +import PageHeader from '@components/PageHeader.astro'; +import MacOSPicker from '@components/MacOSPicker.astro'; +import { CAPABILITIES, CAPABILITY_CATEGORIES } from '@data/capabilities'; +import { MACOS_RELEASES } from '@data/pricing'; +import { LINKS, SITE } from '@data/site'; + +const newest = MACOS_RELEASES.reduce((a, b) => (b.version > a.version ? b : a)); +const supportedByNewest = CAPABILITIES.filter((c) => c.macos <= newest.version).length; +const oldest = MACOS_RELEASES.reduce((a, b) => (b.version < a.version ? b : a)); +--- + + + +
+ + + {supportedByNewest} of {CAPABILITIES.length} features run on {newest.name} ({ + newest.version + }). + + +
+
+ +
+ { + CAPABILITY_CATEGORIES.map((category) => { + const items = CAPABILITIES.filter((c) => c.category === category.id); + if (items.length === 0) return null; + return ( +
+

{category.label}

+
    + {items.map((capability) => ( +
  • +
    +

    {capability.title}

    + + macOS {capability.macos}+ + +
    +

    {capability.summary}

    + {/* Text, not just colour, and rendered server-side so it is + correct before the script runs and without it. */} +

    + {capability.macos <= newest.version + ? '✓ Supported' + : `Needs macOS ${capability.macos}`} +

    +
  • + ))} +
+
+ ); + }) + } + +
+

About these features

+

+ These are Private API features, which need the helper bundle installed on your server + Mac. Sending and receiving messages, attachments and the rest of the core experience + work without it. Setup is covered in the install guide. +

+

+ The server runs on macOS {oldest.version} and newer. Anything older is not supported. + Questions? Ask us on Discord. +

+
+
+
diff --git a/src/pages/pricing/index.astro b/src/pages/pricing/index.astro index b012b0e..c9a97f2 100644 --- a/src/pages/pricing/index.astro +++ b/src/pages/pricing/index.astro @@ -16,6 +16,7 @@ */ import BaseLayout from '@layouts/BaseLayout.astro'; import PageHeader from '@components/PageHeader.astro'; +import MacOSPicker from '@components/MacOSPicker.astro'; import { PLANS, PRO_FEATURES, FREE_FEATURES, MACOS_RELEASES } from '@data/pricing'; import { LINKS } from '@data/site'; @@ -74,49 +75,14 @@ const newestMacOS = MACOS_RELEASES.reduce((a, b) => (b.version > a.version ? b : minimum macOS version, shown alongside it.

-
- - {/* - appearance-none plus our own chevron. The native arrow sits inside the - right padding, and on a rounded-full pill the 18.5px corner radius - curves in over it, so it reads as crammed against the edge. Drawing it - ourselves also makes it match the FAQ accordion's chevron and follow - the theme, which the native control does not. - */} -
- - -
-

- {PRO_FEATURES.filter((f) => f.macos <= newestMacOS.version).length} of { - PRO_FEATURES.length - } Pro features run on {newestMacOS.name} ({newestMacOS.version}). -

+
+ + + {PRO_FEATURES.filter((f) => f.macos <= newestMacOS.version).length} of { + PRO_FEATURES.length + } Pro features run on {newestMacOS.name} ({newestMacOS.version}). + +
    @@ -147,7 +113,7 @@ const newestMacOS = MACOS_RELEASES.reduce((a, b) => (b.version > a.version ? b : feature.macos <= newestMacOS.version ? 'text-accent' : 'text-muted', ]} > - {feature.macos <= newestMacOS.version ? '✓ Included' : `Needs macOS ${feature.macos}`} + {feature.macos <= newestMacOS.version ? '✓ Supported' : `Needs macOS ${feature.macos}`}

    )) @@ -192,53 +158,3 @@ const newestMacOS = MACOS_RELEASES.reduce((a, b) => (b.version > a.version ? b :
- - From d2d13e01b0b82a7315da757458db4dfae859564c Mon Sep 17 00:00:00 2001 From: zlshames Date: Fri, 18 Sep 2026 16:22:41 -0400 Subject: [PATCH 2/2] Hold the compatibility page back until the v2 server ships The page describes the v2 Swift server, which supports macOS 14 and newer. The server shipping today is v1.x on Node.js, which runs on Mojave through Ventura and has a different capability set. Publishing this now would tell a Ventura user they are unsupported when v1 supports them perfectly well. So it is hidden exactly as /pricing is: out of the header and footer, out of the sitemap, noindex. The route still resolves and is still asserted in the build, so it cannot rot while it waits. The page also now states its own scope -- "BlueBubbles Server 2.0 and newer... earlier macOS releases are supported by the 1.x server, which has a different feature set" -- so it is not silently wrong about which server it describes, whenever it does go live. Launch steps are in a comment at the top of the page. The FAQ's existing tables are untouched and stay the public answer for now, which is correct: they describe the v1 server that people are actually running. Co-Authored-By: Claude Opus 5 --- astro.config.mjs | 7 ++++++- src/data/site.ts | 2 -- src/pages/compatibility/index.astro | 20 +++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index 55ea011..de5ef57 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -59,7 +59,12 @@ export default defineConfig({ sitemap({ // /pricing is not public yet -- it exists only so the URL resolves for // Creem's validation. Drop this exclusion when Pro launches. - filter: (page) => !page.includes('/404') && !page.includes('/pricing'), + // /pricing and /compatibility are not public yet. Drop each exclusion + // when it launches -- /compatibility waits on the v2 server. + filter: (page) => + !page.includes('/404') && + !page.includes('/pricing') && + !page.includes('/compatibility'), serialize(item) { // Under build.format 'preserve', @astrojs/sitemap does not append a // trailing slash to directory-index routes. Normalize so canonical URLs diff --git a/src/data/site.ts b/src/data/site.ts index f99e599..dce73ea 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -43,7 +43,6 @@ export interface NavItem { export const NAV_LINKS: readonly NavItem[] = [ { href: '/downloads/', label: 'Downloads' }, { href: '/install/', label: 'Install' }, - { href: '/compatibility/', label: 'Compatibility' }, { href: '/faq/', label: 'FAQ' }, { href: '/donate/', label: 'Donate' }, // Absolute on purpose: /web is a *separate* Pages deploy @@ -55,7 +54,6 @@ export const FOOTER_LINKS: readonly NavItem[] = [ { href: '/', label: 'Home' }, { href: '/downloads/', label: 'Downloads' }, { href: '/install/', label: 'Install' }, - { href: '/compatibility/', label: 'Compatibility' }, { href: '/faq/', label: 'FAQ' }, { href: LINKS.webApp, label: 'Web App', external: true }, { href: '/donate/', label: 'Donate' }, diff --git a/src/pages/compatibility/index.astro b/src/pages/compatibility/index.astro index 2e08649..f33b030 100644 --- a/src/pages/compatibility/index.astro +++ b/src/pages/compatibility/index.astro @@ -1,4 +1,18 @@ --- +/* + * NOT PUBLIC YET. This page describes the **v2 Swift server**, which supports + * macOS 14 and newer. The server shipping today is v1.x (Node.js), which runs + * on Mojave through Ventura and has a different capability set entirely. + * + * Publishing this before v2 ships would tell a Ventura user they are + * unsupported when v1 supports them. So it is hidden the same way /pricing is: + * no nav or footer link, noindex, excluded from the sitemap. + * + * To launch, once v2 is out: add it back to NAV_LINKS and FOOTER_LINKS in + * src/data/site.ts, drop the `noindex` below, and remove the sitemap exclusion + * in astro.config.mjs. Decide then what the FAQ's existing v1 tables should say, + * since both servers will be in use. + */ import BaseLayout from '@layouts/BaseLayout.astro'; import PageHeader from '@components/PageHeader.astro'; import MacOSPicker from '@components/MacOSPicker.astro'; @@ -16,6 +30,7 @@ const oldest = MACOS_RELEASES.reduce((a, b) => (b.version < a.version ? b : a)); description="Which BlueBubbles Private API features work on your Mac, by macOS version." keywords={SITE.keywords} path="/compatibility/" + noindex > (b.version < a.version ? b : a)); >.

- The server runs on macOS {oldest.version} and newer. Anything older is not supported. + This page describes BlueBubbles Server 2.0 and newer, which runs on macOS { + oldest.version + } and newer. Earlier macOS releases are supported by the 1.x server, which has a + different feature set. Questions? Ask us on