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 + }). + + +
+
+ +
+ { + 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. +

+

+ 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}). + +
- -