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/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/pages/compatibility/index.astro b/src/pages/compatibility/index.astro
new file mode 100644
index 0000000..f33b030
--- /dev/null
+++ b/src/pages/compatibility/index.astro
@@ -0,0 +1,119 @@
+---
+/*
+ * 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';
+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
+ }).
+
+
+
+ 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.
+
+
+ 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 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}).
+
+