diff --git a/content/docs/products/fractal/meta.json b/content/docs/products/fractal/meta.json index 14bb349..15ddbb8 100644 --- a/content/docs/products/fractal/meta.json +++ b/content/docs/products/fractal/meta.json @@ -6,6 +6,7 @@ "environments", "pr-previews", "custom-domains", + "object-storage", "backups", "shell", "cli-reference", diff --git a/content/docs/products/fractal/object-storage.mdx b/content/docs/products/fractal/object-storage.mdx new file mode 100644 index 0000000..81f089f --- /dev/null +++ b/content/docs/products/fractal/object-storage.mdx @@ -0,0 +1,100 @@ +--- +title: Object Storage +description: S3-compatible buckets for uploads, assets, and media, with optional public URLs +--- + +Fractal gives every project **S3-compatible object storage**: durable buckets for +user uploads, exports, generated files, and media. Buckets are provisioned from +the dashboard, work with any standard S3 client, and can be **private** (the +default) or **public** with a ready-to-use media URL. + +## Create a bucket + +1. Open your project and go to the **Storage** tab. +2. Click **New bucket** and give it a name. +3. Choose **Private** or **Public** (see below), then create it. + +Bucket names are a single DNS label: 3 to 63 characters, lowercase letters, +digits, and hyphens, with no leading or trailing hyphen (for example +`app-uploads` or `user-avatars`). The bucket is ready within a few seconds. + +## Private vs public buckets + +- **Private** (default): objects are reachable only with the bucket's + credentials. Serve files to users by generating presigned URLs from your app, + or by proxying them through your backend. Use this for anything sensitive. +- **Public**: objects are readable by anyone at a stable URL, with no + credentials. A public bucket is served at `https://.media.omni.dev`, so + an object stored at `avatars/jane.png` in the `user-avatars` bucket is served + at `https://user-avatars.media.omni.dev/avatars/jane.png`. Use this for + avatars, images, video, and other assets you want browsers to load directly. + +Writes always require the bucket's credentials, whether the bucket is public or +private. Public only affects read access. + +You can switch a bucket between private and public later, and its public URL +starts or stops serving accordingly. + +## Connect a bucket to your app + +Attach a bucket to a service and Fractal injects its credentials into the +service as environment variables, so no secrets are ever written into your repo +or image. A service receives: + +- an **endpoint** (the S3 API URL to point your client at), +- the **bucket** name, +- an **access key ID** and **secret access key**, +- a **region**. + +Point any standard S3 client (the AWS SDK, `aws s3`, `rclone`, and so on) at the +endpoint with those credentials and the bucket behaves like any other S3 bucket. +A minimal upload with the AWS SDK for JavaScript: + +```ts +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; + +const s3 = new S3Client({ + endpoint: process.env.S3_ENDPOINT, + region: process.env.S3_REGION, + credentials: { + accessKeyId: process.env.S3_ACCESS_KEY_ID, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, + }, + forcePathStyle: true, +}); + +await s3.send( + new PutObjectCommand({ + Bucket: process.env.S3_BUCKET, + Key: "avatars/jane.png", + Body: file, + ContentType: "image/png", + }), +); +``` + +Set `forcePathStyle: true` (path-style addressing); the platform serves buckets +by path (`/bucket/key`) rather than by virtual host. + +## Serving public media + +For a public bucket, the object's public URL is simply its key appended to the +bucket's media host: + +``` +https://.media.omni.dev/ +``` + +Public objects are served over HTTPS with a valid certificate and hardened +response headers (a restrictive content-security-policy plus `nosniff`, so a +stored file cannot execute as script), and permissive CORS for `GET`/`HEAD` so +browsers and canvases can load them. There is nothing else to configure: upload +with your credentials, then link to the public URL. + +## Limits + +Each plan includes an object-storage allowance: a maximum number of buckets per +project and a total-bytes ceiling across them. Storage above the included +allowance is billed per gigabyte-month (see [Billing](./billing)). Buckets are +deleted from the **Storage** tab; deleting a bucket removes its objects +permanently. diff --git a/content/docs/products/herald/deliverability.mdx b/content/docs/products/herald/deliverability.mdx index 3b5d1be..16e8f01 100644 --- a/content/docs/products/herald/deliverability.mdx +++ b/content/docs/products/herald/deliverability.mdx @@ -38,4 +38,3 @@ For each sending domain, Herald surfaces recommendations in the dashboard: verif ## Delivery events Every message's lifecycle (delivered, bounced, complained, opened, clicked) is tracked in the message log and streamed out as CloudEvents. Watching bounce and complaint events is the fastest way to catch a deliverability issue as it develops. See [Transactional Email](/products/herald/transactional-email) for webhook setup. - diff --git a/content/docs/products/herald/getting-started.mdx b/content/docs/products/herald/getting-started.mdx index f487fc0..9125abc 100644 --- a/content/docs/products/herald/getting-started.mdx +++ b/content/docs/products/herald/getting-started.mdx @@ -71,4 +71,3 @@ Herald runs suppression checks and DKIM signing before the message goes out. Onc - [Transactional Email](/products/herald/transactional-email): batch sends, listing and inspecting messages, and webhooks - [Marketing Email](/products/herald/marketing-email): audiences and broadcasts - [Deliverability](/products/herald/deliverability): dedicated IPs, suppression, and reputation isolation - diff --git a/content/docs/products/herald/index.mdx b/content/docs/products/herald/index.mdx index 3603780..10d2857 100644 --- a/content/docs/products/herald/index.mdx +++ b/content/docs/products/herald/index.mdx @@ -72,5 +72,3 @@ Herald is made up of two parts: - The **API** is what your application sends through: a tenant-scoped GraphQL endpoint that accepts messages, signs and suppresses them, and records every delivery event. - The **dashboard** is where your team works: manage sending domains and their deliverability recommendations, browse the message log and delivery events, manage suppressions and API keys, and see sending health at a glance. - - diff --git a/content/docs/products/herald/marketing-email.mdx b/content/docs/products/herald/marketing-email.mdx index 644d4a4..76569d0 100644 --- a/content/docs/products/herald/marketing-email.mdx +++ b/content/docs/products/herald/marketing-email.mdx @@ -40,4 +40,3 @@ Every broadcast carries the pieces that keep marketing mail compliant: - **CAN-SPAM compliant footers** included automatically. Unsubscribes and complaints feed Herald's suppression list, so a contact who opts out or reports a message is not sent to again. See [Deliverability](/products/herald/deliverability) for how suppression protects your sending reputation. - diff --git a/content/docs/products/herald/transactional-email.mdx b/content/docs/products/herald/transactional-email.mdx index 0e574b9..ddb38de 100644 --- a/content/docs/products/herald/transactional-email.mdx +++ b/content/docs/products/herald/transactional-email.mdx @@ -71,4 +71,3 @@ Every SDK method is a typed wrapper over the generated GraphQL client. For any o ```ts herald.raw.sendEmail({ input: { /* ... */ } }); ``` - diff --git a/content/docs/products/thrivestream.mdx b/content/docs/products/thrivestream.mdx deleted file mode 100644 index 10f6298..0000000 --- a/content/docs/products/thrivestream.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: 📡 Thrivestream -description: Live streaming for creators, with native Threads integration. Watch on any account; apply to go live. ---- - -import { ProductOverview } from "@/components/docs"; -import { FaVideo, FaUsers, FaHashtag } from "react-icons/fa"; - -, - className: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300", - }, - { - label: "Creators", - icon: , - className: "bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300", - }, - { - label: "Threads Integration", - icon: , - className: "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300", - }, - ]} - links={[ - { href: "https://thrivestream.live", label: "Visit Website", type: "website" }, - { href: "https://docs.thrivestream.live", label: "Read the Docs", type: "docs" }, - ]} -/> - -**Thrivestream** is live streaming built for creators. Broadcasters go live from a web studio, viewers watch in real time over WebRTC, and stream activity can mirror to and from [Threads](https://www.threads.net). - -Anyone can sign in to watch and follow. Going live is in early access: streaming is gated to approved creators while Thrivestream is in its founding-streamer phase. - - - Thrivestream keeps its own documentation, which is the single source of truth. **[Read the full Thrivestream docs at docs.thrivestream.live](https://docs.thrivestream.live)** for getting started, going live, chat, guests, Threads, and troubleshooting. - - - - Want to go live? **[Apply to become a streamer](https://thrivestream.live/apply)**. Watching and following are open to everyone; only broadcasting requires approval during early access. - diff --git a/content/docs/products/thrivestream/.mirror.json b/content/docs/products/thrivestream/.mirror.json new file mode 100644 index 0000000..6033e0b --- /dev/null +++ b/content/docs/products/thrivestream/.mirror.json @@ -0,0 +1,4 @@ +{ + "repo": "git@github.com:coopbri/thrivestream-docs.git", + "ref": "3a79734ef0fddc2054626b4b9c73e328e89b7cad" +} diff --git a/content/docs/products/thrivestream/chat.mdx b/content/docs/products/thrivestream/chat.mdx new file mode 100644 index 0000000..3c4b6a6 --- /dev/null +++ b/content/docs/products/thrivestream/chat.mdx @@ -0,0 +1,27 @@ +--- +title: Live Chat +description: Real-time chat alongside every Thrivestream broadcast. +canonical: "https://docs.thrivestream.live/chat" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/chat.mdx" +--- + +Live chat brings a real-time conversation to every stream, between the broadcaster and viewers, updating instantly for everyone watching. + +## Sending Messages + +Type in the chat box and send. Your message appears immediately for everyone in the stream, labeled with your handle. + +## Rate Limits + +To keep chat readable, sending is rate limited: a short burst is fine, but rapid-fire messages are throttled. If you are posting too quickly, wait a moment and try again. + +## Moderation + +On your own stream, each chat message shows a **Remove from chat** button (only you, the broadcaster, can see it). Selecting it removes that person from your chat: their existing messages stay, but they can no longer post for the rest of the broadcast. Keep it respectful: harassment and spam are not welcome. + + + Live chat is tied to a live stream: once the broadcast ends, no new messages + can be sent. If the stream was recorded, the conversation lives on as [chat + replay](/products/thrivestream/videos#chat-replay) on the video, synced to playback. + diff --git a/content/docs/products/thrivestream/following.mdx b/content/docs/products/thrivestream/following.mdx new file mode 100644 index 0000000..4feca10 --- /dev/null +++ b/content/docs/products/thrivestream/following.mdx @@ -0,0 +1,29 @@ +--- +title: Following +description: Follow creators and reach followers-only streams on Thrivestream. +canonical: "https://docs.thrivestream.live/following" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/following.mdx" +--- + +Following a creator is how you keep up with the people you want to watch, and how creators build an audience they can broadcast to directly. + +## Follow a Creator + +Open a creator's channel at `/@their-handle` (for example from a stream's byline) and select **Follow**. The button switches to **Following**, and the creator's follower count updates right away. Select it again to unfollow. + +You can't follow yourself, and the Follow button doesn't appear on your own channel. + +## Follower Count + +Every channel shows how many people follow that creator. It updates as people follow and unfollow. + +## Followers-Only Streams + +Creators can limit a broadcast to their followers by choosing **Followers only** when they [go live](/products/thrivestream/going-live#who-can-watch). If you open a followers-only stream you don't follow yet, you'll see a **Follow to watch** prompt: follow the creator and playback begins. See [Watching](/products/thrivestream/watching#followers-only-streams) for what this looks like as a viewer. + + + Following is separate from the **Follow on Threads** link on a channel. That + link points to the creator's [Threads](/products/thrivestream/threads) profile; following on + Thrivestream is what unlocks their followers-only streams. + diff --git a/content/docs/products/thrivestream/getting-started.mdx b/content/docs/products/thrivestream/getting-started.mdx new file mode 100644 index 0000000..79fdbd6 --- /dev/null +++ b/content/docs/products/thrivestream/getting-started.mdx @@ -0,0 +1,34 @@ +--- +title: Getting Started +description: Sign in and find your way around Thrivestream. +canonical: "https://docs.thrivestream.live/getting-started" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/getting-started.mdx" +--- + +## Sign In + +Thrivestream uses an Omni account to sign in. + +1. Open [thrivestream.live](https://thrivestream.live) +2. Select **Continue with Omni** +3. Sign in to Omni, or create an account if you do not have one +4. You land on the live feed, signed in + +If you used Thrivestream before sign-in moved to Omni, your account is still there. If you are still signed in on a device, connect your Omni account from [Settings](/products/thrivestream/settings) and **Continue with Omni** will sign you in from then on. If you are locked out, reach out and we will connect it for you. See [Omni Account](/products/thrivestream/omni-account). + + + Thrivestream is in private testing, so sign-in is limited to people who have + been invited. If you cannot get in, you have not been added yet. + + +## The Live Feed + +The home page is the **live feed**: every stream that is on the air right now. Select a stream to start watching, or head to the studio to go live yourself. + +## Next Steps + +- Connect your Omni account for sign-in: see [Omni Account](/products/thrivestream/omni-account) +- Ready to broadcast? See [Going Live](/products/thrivestream/going-live) +- Want to watch first? See [Watching](/products/thrivestream/watching) +- Curious how Threads ties in? See [Threads Integration](/products/thrivestream/threads) diff --git a/content/docs/products/thrivestream/going-live.mdx b/content/docs/products/thrivestream/going-live.mdx new file mode 100644 index 0000000..3b76e0a --- /dev/null +++ b/content/docs/products/thrivestream/going-live.mdx @@ -0,0 +1,94 @@ +--- +title: Going Live +description: Start a broadcast from the Thrivestream web studio. +canonical: "https://docs.thrivestream.live/going-live" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/going-live.mdx" +--- + +You broadcast from the **studio**, a web page that runs entirely in your browser. There is nothing to download. + +## Start a Stream + +1. From the live feed, select **Go Live** +2. Give your stream a title (and an optional description) +3. Choose your [orientation](#orientation), Landscape or Portrait +4. Under **Who can watch**, choose your stream's audience (see [below](#who-can-watch)) +5. Select **Go live** to open your studio, then select **Enter studio** +6. Grant your browser access to your camera and microphone when prompted +7. You land in a private [preview](#preview-your-control-room). Get your shot right, then select **Go Live** to go on the air + +A short **countdown** plays once you commit, then you go live. Nothing is public until the countdown finishes: your stream stays off the feed and no Threads announcement goes out until you are actually on the air. + +Your stream now appears in the live feed, and viewers can join. + + + Want a copy afterward? Toggle **Record this stream** before you go live and + Thrivestream saves it as a [video](/products/thrivestream/videos) you can share and download. + + +## Orientation + +Choose how your video is framed before you go live: + +- **Landscape** (16:9) is widescreen, best for desktop, gaming, and webinars +- **Portrait** (9:16) is vertical, best for phones and mobile-first viewers + +Thrivestream starts on a default that suits your device (Portrait on phones, Landscape on larger screens), and you can change it in the go-live form or on the studio's ready screen. You set orientation before you go live, so it can't change once you are on the air, and any recording keeps the same shape. + +## Who Can Watch + +Every stream has a visibility that you pick when you create it. It controls who +can find and join your broadcast: + +- **Public**: anyone can find your stream in the live feed and watch it. This is the default. +- **Ghost (Unlisted)**: your stream goes invisible in the feed and is reachable only by its direct link (that's the ghost). Handy for sharing a broadcast with a specific group. +- **Followers only**: only people who [follow you](/products/thrivestream/following) can watch. Anyone else who opens the link sees a prompt to follow first. + +Ghost and followers-only streams never appear in the public feed. You can share their link the same way as any other stream. + +## Preview (Your Control Room) + +Before anything goes public, the studio drops you into a private **preview**. It looks just like the live studio, with a clear **Preview** badge, and only you can see it: + +- Watch your real self-view, exactly as viewers will once you go live +- Set your framing: [switch cameras](#camera), zoom, and check your mic level +- Toggle your camera or share your screen (on desktop) to rehearse your setup + +Nothing is broadcast here. Your stream stays off the feed and no Threads announcement fires until you commit. When you are ready, select **Go Live** to start the countdown, or **Back to setup** to step back out and adjust your details. Because you go live from a warm preview, your camera is already connected, so you go on the air without a cold start. + +## In the Studio + +While you are live, the studio shows: + +- **Your preview**, exactly what viewers see +- **Controls** to mute your microphone, toggle your camera, share your screen (on desktop), and end the stream +- A **share button** to copy your stream link (or open the share sheet on mobile) so you can invite people in +- A **Guests** button to bring people on stage with you (see [Streaming with Guests](/products/thrivestream/guests)) + +[Live chat](/products/thrivestream/chat) runs right in the studio, so you can talk with your viewers and moderate the conversation while you broadcast. + +## Camera + +- **Switch cameras**: on a phone with more than one camera, a switch-camera button flips between the front and rear lens, both before you go live and mid-broadcast. On the ready screen you can also pick which camera you start on +- **Zoom**: pinch on your video (or use the on-screen zoom buttons) to zoom the camera in and out +- **Toggle**: turn your camera off and back on at any time from the studio controls + +Camera controls appear only where they apply: the switch button is hidden when your device has a single camera, and zoom shows where your camera or device supports it. + +## End a Stream + +Select **End Stream** in the studio. The broadcast stops immediately and the stream leaves the live feed. + + + You don't have to end perfectly: if you close or reload the studio while live, + your browser warns you first, and your stream keeps running for a short time so + you can return and pick up where you left off. If you don't come back, it ends + automatically. + + +## Tips for a Good Broadcast + +- Use a wired connection when you can; WebRTC is sensitive to unstable upload bandwidth +- Find a quiet, well-lit spot +- Share your stream link and let your Threads audience know you are live diff --git a/content/docs/products/thrivestream/guests.mdx b/content/docs/products/thrivestream/guests.mdx new file mode 100644 index 0000000..8015f3b --- /dev/null +++ b/content/docs/products/thrivestream/guests.mdx @@ -0,0 +1,77 @@ +--- +title: Streaming with Guests +description: Bring people on stage during your broadcast with invite links and a backstage green room. +canonical: "https://docs.thrivestream.live/guests" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/guests.mdx" +--- + +You don't have to stream alone. Invite someone to join your broadcast and bring them **on stage**, where they appear alongside you and everyone watching sees and hears them. A stream can have you plus up to **three guests** on stage at once. + +Guests go through a private **backstage** green room first, so you can get them set up before they are live. Nobody watching sees a guest until you bring them on. + +## Invite a Guest (Host) + +1. Go live as usual (see [Going Live](/products/thrivestream/going-live)) +2. In the studio, select the **Guests** button +3. Select **Copy invite link** +4. Send that link to your guest however you like (a message, a Threads reply, email) + +Each invite link is valid for **24 hours**. Anyone with the link can join backstage, so share it only with the person you want on your stream. + +## Backstage and On Stage + +Your guest opens the link, signs in, and lands **backstage**, a private room only you and your guests can see. There they can check their camera and microphone before going live. + +Open the **Guests** panel to manage everyone: + +- **Requests** lists viewers who asked to come on (see [Ask to Join](#ask-to-join-a-stream-viewer)). **Approve** sends them to the green room; decline with the X +- **Backstage** lists the guests waiting in the green room +- **On stage** lists the guests currently live, with a counter showing how many of your slots are used + +Select **Preview** in the Backstage section to see and hear your waiting guests live before you bring them on, so you can check their shot and sound while they are still private to you. + +For each guest you can: + +- **Bring on**: move a backstage guest on stage so viewers see them. This is disabled once the stage is full; send someone backstage to free a slot +- **Send backstage**: take an on-stage guest back to the green room without removing them +- **Audio only**: switch a guest to microphone only (no camera), for a call-in style guest. Select it again to re-enable their camera +- **Mute**: silence an on-stage guest's microphone +- **Remove**: drop a guest from the stream entirely + + + Guests on stage are part of your broadcast, so if you are [recording](/products/thrivestream/videos) + the stream, they appear in the recording too. The backstage room is never + recorded. + + +## Ask to Join a Stream (Viewer) + +You don't always need an invite. On a live stream, select **Request to join** to ask the host to bring you on: + +1. On the stream page, select **Request to join** +2. The button shows **Waiting for the host** while your request is pending +3. When the host approves, you go straight to the **backstage** green room, the same as an invited guest, and the host can bring you on stage from there +4. If the host declines, the button shows **Request declined** + +The host only ever sees your request in their **Requests** list; other viewers don't see who asked. + +## Joining as a Guest + +If someone sends you an invite link: + +1. Open the link and sign in with your [Omni account](/products/thrivestream/omni-account) if you aren't already +2. You arrive **backstage**. Grant your browser access to your camera and microphone +3. Get your shot and sound right. Nobody watching can see or hear you yet +4. When the host brings you on, you go **on stage** automatically and the audience can see you +5. Use the on-screen controls to toggle your camera or microphone, or **Leave** when you're done + +If the host sends you backstage again, you drop out of view and wait in the green room until they bring you back on. If they remove you or the stream ends, your guest session closes and you can keep watching from the stream page. + +If the host sets you to **audio only**, your camera turns off and you join with just your microphone. Your camera control disappears until they re-enable it. + +## Good to Know + +- Guests need an Omni account, since the invite signs them in to identify them on your stream +- Only you, the host, can bring guests on, mute, or remove them +- A guest is only ever visible to viewers while on stage; backstage stays private to you and your guests diff --git a/content/docs/products/thrivestream/index.mdx b/content/docs/products/thrivestream/index.mdx new file mode 100644 index 0000000..948cb90 --- /dev/null +++ b/content/docs/products/thrivestream/index.mdx @@ -0,0 +1,36 @@ +--- +title: Introduction +description: Learn about Thrivestream, live streaming for creators. +canonical: "https://docs.thrivestream.live" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/index.mdx" +--- + + + These docs mirror [docs.thrivestream.live](https://docs.thrivestream.live), the canonical Thrivestream documentation. Want to go live? **[Apply to become a streamer](https://thrivestream.live/apply)**. + + +Thrivestream is live streaming built for creators. Broadcasters go live from a web studio, viewers watch in real time over WebRTC, and stream activity can mirror to and from [Threads](https://www.threads.net). + +## How It Works + +1. **Sign in** with your Omni account +2. **Browse the live feed** to see who is streaming right now +3. **Go live** from the web studio with your camera and microphone +4. **Viewers join** and watch in real time over low-latency WebRTC +5. **Your stream connects to Threads** so your audience can follow along + +## Features + +- **[Omni Account](/products/thrivestream/omni-account)**: connect an Omni account so sign-in no longer depends on Threads +- **[Going Live](/products/thrivestream/going-live)**: start a broadcast from the web studio, no software to install +- **[Streaming with Guests](/products/thrivestream/guests)**: invite people on stage with a backstage green room +- **[Watching](/products/thrivestream/watching)**: join any live stream and watch over WebRTC +- **[Following](/products/thrivestream/following)**: follow creators and reach their followers-only streams +- **[Live Chat](/products/thrivestream/chat)**: real-time chat alongside every stream, with broadcaster moderation +- **[Threads Integration](/products/thrivestream/threads)**: connect Threads to announce streams and mirror chat to your audience +- **[Settings](/products/thrivestream/settings)**: manage your profile and preferences + +## Getting Started + +New to Thrivestream? Start with the [Getting Started](/products/thrivestream/getting-started) guide. diff --git a/content/docs/products/thrivestream/meta.json b/content/docs/products/thrivestream/meta.json new file mode 100644 index 0000000..7bc7860 --- /dev/null +++ b/content/docs/products/thrivestream/meta.json @@ -0,0 +1,17 @@ +{ + "title": "📡 Thrivestream", + "pages": [ + "index", + "getting-started", + "omni-account", + "going-live", + "guests", + "watching", + "following", + "videos", + "chat", + "threads", + "settings", + "troubleshooting" + ] +} diff --git a/content/docs/products/thrivestream/omni-account.mdx b/content/docs/products/thrivestream/omni-account.mdx new file mode 100644 index 0000000..61dcba7 --- /dev/null +++ b/content/docs/products/thrivestream/omni-account.mdx @@ -0,0 +1,61 @@ +--- +title: Omni Account +description: Connect an Omni account so your sign-in no longer depends on Threads. +canonical: "https://docs.thrivestream.live/omni-account" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/omni-account.mdx" +--- + +Thrivestream sign-in runs on **Omni accounts**. If you joined before the switch, connecting yours takes about a minute and changes nothing else about your account. + + + Threads sign-in has been retired. If you are still signed in from before the + switch, connect your Omni account now to keep access. If you are already + locked out, reach out with your Omni username and we will connect it for you. + + +## Why This Changed + +Sign-in used to go through Threads, which meant a problem on Threads' side could keep you out of your own account even when Thrivestream itself was working perfectly. + +An Omni account is your own sign-in, independent of any social network. Threads stays connected for the things it is actually good at, like announcing your stream to your audience. It just stops being the front door. + +## Connect Your Account + +Do this while you are still signed in from before the switch. + +1. Open Thrivestream on a device where you are still signed in +2. Open **Settings** +3. Under **Omni account**, select **Connect Omni account** +4. Sign in to Omni, or create an account if you do not have one +5. You return to Settings, and the section now reads **Connected** + +That is the whole process. You only do it once. + +If you are already signed out everywhere and never connected Omni, you cannot do this yourself. Reach out with your Omni username and we will attach it to your existing profile by hand, then **Continue with Omni** will sign you in. See [sign-in troubleshooting](/products/thrivestream/troubleshooting/sign-in). + +## What Does Not Change + +Connecting an Omni account attaches it to the profile you already have. It does not create a second account, so: + +- Your streams, videos and followers stay with you +- Your avatar and profile stay as they are +- Your Threads connection keeps working, including go-live announcements and chat mirroring +- Nothing about your Threads account itself is altered + +## Your Handle + +Your Thrivestream handle is now your **Omni username**, and your channel lives at **thrivestream.live/@yourusername**. If your Omni username already matches your old Threads handle, nothing changes on your side. + +To change your handle later, change your Omni username: it updates on Thrivestream automatically the next time you sign in. Your Thrivestream handle is independent of Threads, so viewers can still find you there through the optional **Follow on Threads** link you set in [Settings](/products/thrivestream/settings). + +## After You Connect + +From now on, sign in with **Continue with Omni**. It reaches the same account, with all of your streams, videos and followers intact. + +Once you have connected, you do not need to do anything else. + +## Next Steps + +- Manage the connection any time from [Settings](/products/thrivestream/settings) +- See what else Threads does for you in [Threads Integration](/products/thrivestream/threads) diff --git a/content/docs/products/thrivestream/settings.mdx b/content/docs/products/thrivestream/settings.mdx new file mode 100644 index 0000000..a7a9bb4 --- /dev/null +++ b/content/docs/products/thrivestream/settings.mdx @@ -0,0 +1,33 @@ +--- +title: Settings +description: Manage your Thrivestream profile and preferences. +canonical: "https://docs.thrivestream.live/settings" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/settings.mdx" +--- + +Open **Settings** from the navigation while signed in to manage your account. + +## Your Profile + +Your display name and avatar come from your **Omni account** and appear on your streams and in chat. To change them, update your Omni profile; the change flows to Thrivestream the next time you sign in. + +## Omni Account + +Sign-in is moving to Omni accounts. If yours is not connected yet, this section shows a **Connect Omni account** button; once connected it reads **Connected**. + +Connecting attaches an Omni account to the profile you already have, so nothing about your streams, videos or followers changes. See [Omni Account](/products/thrivestream/omni-account) for the full walkthrough. + +## Threads + +Connect Threads here to announce your go-lives and mirror your live chat there. Thrivestream uses this connection **only to post on your behalf**: it never reads your Threads profile, followers, or activity. See [Threads Integration](/products/thrivestream/threads) for details. + +This section also has a **Threads handle** field. Enter your handle to add a "Follow on Threads" link to your channel and streams. It's just a link you type in, so it needs no extra access. + +## Preferences + +Settings is where you manage your account-level preferences for Thrivestream. Options expand as the platform grows during testing. + +## Signing Out + +You can sign out from Settings at any time. Signing out of Thrivestream does not affect your Omni account or any connection you have added. diff --git a/content/docs/products/thrivestream/threads.mdx b/content/docs/products/thrivestream/threads.mdx new file mode 100644 index 0000000..cdbc3f1 --- /dev/null +++ b/content/docs/products/thrivestream/threads.mdx @@ -0,0 +1,39 @@ +--- +title: Threads Integration +description: Connect Threads to announce your streams and mirror chat. +canonical: "https://docs.thrivestream.live/threads" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/threads.mdx" +--- + +[Threads](https://www.threads.net) is an optional connection. Sign-in runs on your Omni account; connecting Threads lets your stream activity reach the audience you already have there. + +## Connecting Threads + +Connect Threads from [Settings](/products/thrivestream/settings). It is entirely optional: everything except the Threads features below works without it. See [Omni Account](/products/thrivestream/omni-account) for how sign-in works. + +## Reaching Your Audience + +Once connected, Thrivestream can: + +- **Announce your go-lives**: post a "🔴 LIVE" update to your Threads when you start a stream +- **Mirror your live chat**: reply your stream's chat onto that go-live post so your Threads audience can follow along + +Both are opt-in toggles in [Settings](/products/thrivestream/settings), and both only ever **post on your behalf**. + +## Follow on Threads Link + +In [Settings](/products/thrivestream/settings) you can enter your **Threads handle** to add a "Follow on Threads" link to your channel and streams. This is a handle you type in yourself, so it works whether or not you connect Threads. + + + Threads features roll out gradually during testing. Some integrations may be + limited or gated while Thrivestream is in private beta. + + +## What Thrivestream Reads + +Nothing. The Threads connection is used **only to publish** the posts above. Thrivestream never reads your Threads profile, avatar, username, followers, or activity: your name and avatar come from your [Omni account](/products/thrivestream/omni-account), and your "Follow on Threads" handle is the one you type in Settings. + +## Permissions + +When you connect, Threads asks you to authorize Thrivestream to publish content. You can review and revoke that access at any time from your Threads account settings; sign-in is unaffected, because it does not depend on Threads. diff --git a/content/docs/products/thrivestream/troubleshooting/meta.json b/content/docs/products/thrivestream/troubleshooting/meta.json new file mode 100644 index 0000000..a9a0c7e --- /dev/null +++ b/content/docs/products/thrivestream/troubleshooting/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Troubleshooting", + "pages": ["sign-in", "streaming"] +} diff --git a/content/docs/products/thrivestream/troubleshooting/sign-in.mdx b/content/docs/products/thrivestream/troubleshooting/sign-in.mdx new file mode 100644 index 0000000..48d990a --- /dev/null +++ b/content/docs/products/thrivestream/troubleshooting/sign-in.mdx @@ -0,0 +1,41 @@ +--- +title: Sign in +description: Fix common problems signing in to Thrivestream. +canonical: "https://docs.thrivestream.live/troubleshooting/sign-in" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/troubleshooting/sign-in.mdx" +--- + +Thrivestream signs you in with an **Omni account**. On the sign-in page, select **Continue with Omni**. There is no longer a "Sign in with Threads" option: Threads stays connected only for the things it is good at, like announcing your stream and mirroring chat, and is no longer the front door. + +## "Connect your account first" + +You signed in with an Omni account that is not attached to your Thrivestream profile yet, so there was no account to sign you in to. Creating a new one would leave your streams, videos and followers behind on the old profile, so Thrivestream stops instead of splitting your account in two. + +This happens to people who used Thrivestream before sign-in moved to Omni and have not linked an Omni account yet. + +**If you are still signed in on any device**, open Thrivestream there, go to **Settings**, and under **Omni account** select **Connect Omni account**. That attaches Omni to the profile you already have, and from then on **Continue with Omni** signs you straight in. See [Omni Account](/products/thrivestream/omni-account) for the full walkthrough. + +**If you are signed out everywhere**, reach out with your Omni username and we will connect it to your existing streams by hand, then you can sign in again. See [Still stuck?](#still-stuck) below. + +## "Invite only for now" + +Thrivestream is in private testing, so sign-in is limited to people who have been invited. If you see this, your account has not been added yet. Reach out if you think it should have been. + +## Sign-in said "something went wrong" + +That is usually a brief connection hiccup, not a problem with your account. Try again, and it normally goes through on the second attempt. A few things that help if it keeps happening: + +- Open [thrivestream.live](https://thrivestream.live) in Safari or Chrome directly, not inside another app's browser +- If a sign-in window opens and stalls, close it and start again +- Finish the sign-in in the window that opens rather than closing it +- If it keeps failing on your phone, try a laptop or desktop +- On Android, if a popup is blocked, try a different browser or allow popups for Thrivestream + +## Is my data still there? + +Yes. Nothing was deleted when sign-in moved to Omni. Your streams, videos, followers and profile are all attached to the same account and are waiting for you once you are back in. + +## Still Stuck? + +Reach out on Threads at [@coop_bri](https://www.threads.net/@coop_bri) with your Omni username. diff --git a/content/docs/products/thrivestream/troubleshooting/streaming.mdx b/content/docs/products/thrivestream/troubleshooting/streaming.mdx new file mode 100644 index 0000000..fa71ff3 --- /dev/null +++ b/content/docs/products/thrivestream/troubleshooting/streaming.mdx @@ -0,0 +1,31 @@ +--- +title: Streaming +description: Fixes for camera, microphone, and stream playback issues. +canonical: "https://docs.thrivestream.live/troubleshooting/streaming" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/troubleshooting/streaming.mdx" +--- + +## My Camera or Microphone Will Not Start + +Going live needs browser permission for your camera and microphone: + +- Grant access when your browser prompts you +- If you dismissed the prompt, re-enable camera and microphone for the site in your browser settings, then reload +- Make sure no other app is holding the camera + +## The Stream Keeps Buffering + +Thrivestream streams over WebRTC, which is sensitive to network quality: + +- Use a wired connection or a strong Wi-Fi signal +- Close other apps that are using a lot of bandwidth +- Broadcasters: a stable upload speed matters more than a fast download + +## My Stream Ended On Its Own + +Leaving the studio does not end your stream right away. If you close or reload the tab while live, your browser warns you first, and the stream keeps running for a short time so you can return and resume from the studio. If you stay away too long, it ends automatically. A dropped network connection reconnects on its own; if it cannot recover, return to the studio and select **Reconnect**. + +## Still Stuck? + +Reach out on Threads at [@coop_bri](https://www.threads.net/@coop_bri). diff --git a/content/docs/products/thrivestream/videos.mdx b/content/docs/products/thrivestream/videos.mdx new file mode 100644 index 0000000..3ffdf94 --- /dev/null +++ b/content/docs/products/thrivestream/videos.mdx @@ -0,0 +1,51 @@ +--- +title: Videos +description: Record your streams, then share and download them after you go live. +canonical: "https://docs.thrivestream.live/videos" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/videos.mdx" +--- + +Turn a stream into a **video** you can rewatch, share, and download after you go live. + +## Record a Stream + +Before you go live, toggle **Record this stream** on. Thrivestream captures the broadcast and, once you end it, saves it as a video. + + + Recording is per-stream and off by default, so you only keep the streams you + choose to. + + +## Find Your Videos + +After a stream ends, open **Videos** from the top nav. Your recording shows up there once it finishes processing, usually within a minute of ending. + +## Chat Replay + +Open a video and the [live chat](/products/thrivestream/chat) from that stream replays right beside the player, synced to playback. Each message reappears at the moment it was originally sent, so scrubbing forward or back moves the chat with the video. Chat replay is read-only: it shows the conversation as it happened, with no way to add new messages. If the stream had no chat, the panel simply stays empty. + +## Share a Video + +Your videos are **private by default** — only you can see them. To make one shareable: + +1. Open the video +2. Select **Publish** + +Now anyone with the link can watch it, and it appears in Videos for others. Drop the link anywhere (like Threads) to bring your audience back: every video links to the creator's Threads profile, so viewers can follow you. + +Changed your mind? **Make private** hides it again, and **Delete** removes it for good. + +## Your Channel + +Every creator has a channel at **thrivestream.live/@yourhandle** listing your live streams and published videos in one place. Share it to point people at everything you're streaming. + +## Download + +Select **Download** on one of your videos to save an MP4 to your device, handy for reposting elsewhere. + +## How Long Videos Are Kept + +By default, videos are kept for **90 days**, then automatically deleted. Each video shows how long it has left. + +Want to hold onto one? Select **Keep** on your video and it stays indefinitely, with no countdown, until you release it or delete it. Select **Release** to put it back on the normal schedule. You can also **Download** a copy to your device at any time. diff --git a/content/docs/products/thrivestream/watching.mdx b/content/docs/products/thrivestream/watching.mdx new file mode 100644 index 0000000..32f60aa --- /dev/null +++ b/content/docs/products/thrivestream/watching.mdx @@ -0,0 +1,37 @@ +--- +title: Watching +description: Join a live stream and watch in real time. +canonical: "https://docs.thrivestream.live/watching" +mirroredFrom: "git@github.com:coopbri/thrivestream-docs.git@3a79734ef0fddc2054626b4b9c73e328e89b7cad" +sourceUrl: "https://github.com/coopbri/thrivestream-docs/blob/3a79734ef0fddc2054626b4b9c73e328e89b7cad/content/docs/watching.mdx" +--- + +Watching on Thrivestream is instant. Select any stream in the live feed and it begins playing over WebRTC, the same low-latency technology used for video calls. + +## Join a Stream + +1. Open the [live feed](https://thrivestream.live) +2. Select a stream that is on the air +3. The broadcast starts playing right away + +## While Watching + +- **Low latency** means you see the stream within moments of it happening +- **Share** the stream with a tap (native share on mobile, copy link on desktop) to bring friends in +- **[Live chat](/products/thrivestream/chat)** alongside the video lets you join the conversation in real time +- **Guests** the host brings on stage appear right in the video alongside the broadcaster (see [Streaming with Guests](/products/thrivestream/guests)) + +## Followers-Only Streams + +Some creators limit a stream to their followers. When you open a **followers-only** stream that you don't follow yet, you'll see a **Follow to watch** prompt instead of the video. Select it to [follow the creator](/products/thrivestream/following), and playback starts right away. If you're signed out, you'll be asked to sign in first. + +Public and [ghost (unlisted)](/products/thrivestream/going-live#who-can-watch) streams have no such gate: anyone with access to the stream can watch. + +## When a Stream Ends + +When the broadcaster ends their stream, playback stops and the stream leaves the feed. Head back to the feed to find another live broadcast. + + + Streams are live only. If you arrive after a broadcast has ended, it will no + longer appear in the feed. + diff --git a/package.json b/package.json index d0fb00e..fcdedf0 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "bun run scripts/generateCatalog.ts && vite build", + "build": "bun run scripts/syncProductDocs.ts && bun run scripts/generateCatalog.ts && vite build", "catalog:generate": "bun run scripts/generateCatalog.ts", + "docs:sync": "bun run scripts/syncProductDocs.ts", "start": "node .output/server/index.mjs", "format": "biome format --write", "lint": "biome lint", diff --git a/scripts/productDocs.config.ts b/scripts/productDocs.config.ts new file mode 100644 index 0000000..459ef11 --- /dev/null +++ b/scripts/productDocs.config.ts @@ -0,0 +1,49 @@ +/** + * Product docs mirror manifest. + * + * docs.omni.dev is an aggregator, not the source of truth. Each product that + * keeps its own docs owns them in its own repo; this manifest lists which of + * those to mirror into `content/docs/products//` at build time so the docs + * surface here without duplicating authorship. See `syncProductDocs.ts`. + * + * The single source of truth stays with each product. Bump `ref` (and re-run + * `bun run docs:sync`) to publish new upstream content; the mirrored files are + * committed so a build without repo access ships the last good copy. + */ + +export interface ProductDocsMirror { + /** Product id; content mirrors into `content/docs/products//` */ + id: string; + /** Sidebar/section label for the mirrored folder */ + title: string; + /** Git URL of the product's own docs repo (the source of truth) */ + repo: string; + /** Pinned commit to mirror; bump to publish new upstream content */ + ref: string; + /** Path within the repo to the docs content root */ + contentDir: string; + /** Canonical docs origin, e.g. https://docs.thrivestream.live */ + canonicalOrigin: string; + /** + * Optional MDX prepended to the section index, for Omni-side framing (an + * aggregator note, an apply CTA). Kept in the manifest so the mirror stays a + * verbatim copy the sync never has to hand-edit. + */ + indexBanner?: string; +} + +export const productDocsMirrors: ProductDocsMirror[] = [ + { + id: "thrivestream", + title: "📡 Thrivestream", + repo: "git@github.com:coopbri/thrivestream-docs.git", + ref: "3a79734ef0fddc2054626b4b9c73e328e89b7cad", + contentDir: "content/docs", + canonicalOrigin: "https://docs.thrivestream.live", + indexBanner: [ + '', + " These docs mirror [docs.thrivestream.live](https://docs.thrivestream.live), the canonical Thrivestream documentation. Want to go live? **[Apply to become a streamer](https://thrivestream.live/apply)**.", + "", + ].join("\n"), + }, +]; diff --git a/scripts/syncProductDocs.ts b/scripts/syncProductDocs.ts new file mode 100644 index 0000000..d9434f6 --- /dev/null +++ b/scripts/syncProductDocs.ts @@ -0,0 +1,226 @@ +/** + * Mirror per-product docs into this aggregator. + * + * docs.omni.dev does not author product docs; it aggregates them. For each + * entry in `productDocs.config.ts` this clones the product's own docs repo at a + * pinned ref and copies its content into `content/docs/products//`, so the + * pages render here (search, sidebar, LLM copy) while the source of truth stays + * with the product. Mirrored files are committed, so a build without repo + * access ships the last good copy (mirrors the `generateCatalog` philosophy); + * on a successful fetch the mirror is rewritten from scratch. + * + * Two transforms make an upstream copy fit under `/products//`: + * - root-relative links (`/going-live`) are prefixed with the mount path, since + * upstream authors them for its own root + * - a `canonical` (pointing back to the product's own docs domain) and a + * `mirroredFrom` provenance field are injected into every page's frontmatter + * + * Never hand-edit files under a mirrored folder; edit the upstream repo and bump + * `ref`. + */ + +import { execFileSync } from "node:child_process"; +import { + cpSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, relative, resolve } from "node:path"; + +import { productDocsMirrors } from "./productDocs.config"; + +import type { ProductDocsMirror } from "./productDocs.config"; + +const contentRoot = resolve(import.meta.dir, "../content/docs/products"); + +const log = (message: string) => + // biome-ignore lint/suspicious/noConsole: build script output + console.log(`[product-docs] ${message}`); + +const warn = (message: string) => + // biome-ignore lint/suspicious/noConsole: build script output + console.warn(`[product-docs] ${message}`); + +/** Clone a repo at a pinned ref into a throwaway dir, returning its path */ +const checkout = (mirror: ProductDocsMirror): string => { + const dir = mkdtempSync(join(tmpdir(), `omni-docs-${mirror.id}-`)); + execFileSync("git", ["clone", "--quiet", mirror.repo, dir], { + stdio: ["ignore", "ignore", "inherit"], + }); + execFileSync("git", ["-C", dir, "checkout", "--quiet", mirror.ref], { + stdio: ["ignore", "ignore", "inherit"], + }); + return dir; +}; + +/** Turn a mirrored file's relative path into its canonical URL path */ +const urlPath = (relPath: string): string => { + const noExt = relPath.replace(/\.mdx$/, ""); + if (noExt === "index") return ""; + return `/${noExt.replace(/\/index$/, "")}`; +}; + +/** Browsable URL of the upstream source file, for the page's "view source" link */ +const upstreamSourceUrl = ( + mirror: ProductDocsMirror, + relPath: string, +): string => { + const https = mirror.repo + .replace(/^git@([^:]+):/, "https://$1/") + .replace(/\.git$/, ""); + return `${https}/blob/${mirror.ref}/${mirror.contentDir}/${relPath}`; +}; + +/** Prefix upstream root-relative links with the product mount path */ +const rewriteLinks = (source: string, mount: string): string => + source + // markdown links: [text](/path) and [text](/path#anchor) + .replace(/\]\((\/[^)]*)\)/g, (match, href: string) => { + if (href.startsWith("//")) return match; // protocol-relative + if (href === mount || href.startsWith(`${mount}/`)) return match; + return `](${mount}${href})`; + }) + // JSX href="/path" / href='/path' + .replace( + /href=(["'])(\/[^"']*)\1/g, + (match, quote: string, href: string) => { + if (href.startsWith("//")) return match; + if (href === mount || href.startsWith(`${mount}/`)) return match; + return `href=${quote}${mount}${href}${quote}`; + }, + ); + +/** Inject canonical + provenance frontmatter and any index banner */ +const transformMdx = ( + raw: string, + relPath: string, + mirror: ProductDocsMirror, +): string => { + const mount = `/products/${mirror.id}`; + const canonical = `${mirror.canonicalOrigin}${urlPath(relPath)}`; + + const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?/); + if (!fmMatch) { + warn(`no frontmatter in ${relPath}, mirroring body only`); + } + const frontmatter = fmMatch ? fmMatch[1] : ""; + let body = fmMatch ? raw.slice(fmMatch[0].length) : raw; + + body = rewriteLinks(body, mount); + + if (relPath === "index.mdx" && mirror.indexBanner) { + body = `\n${mirror.indexBanner}\n${body}`; + } + + const injected = [ + frontmatter, + `canonical: "${canonical}"`, + `mirroredFrom: "${mirror.repo}@${mirror.ref}"`, + `sourceUrl: "${upstreamSourceUrl(mirror, relPath)}"`, + ] + .filter(Boolean) + .join("\n"); + + return `---\n${injected}\n---\n${body}`; +}; + +/** Copy the section's root meta.json, ensuring a titled folder label */ +const writeMeta = ( + srcMeta: string | undefined, + destDir: string, + title: string, +) => { + const meta = srcMeta ? JSON.parse(srcMeta) : { pages: [] }; + // the aggregator owns the folder label; upstream title (if any) is overridden + const merged = { title, ...meta }; + merged.title = title; + writeFileSync( + join(destDir, "meta.json"), + `${JSON.stringify(merged, null, 2)}\n`, + ); +}; + +/** Recursively mirror .mdx and meta.json from src into dest with transforms */ +const mirrorTree = ( + srcDir: string, + destDir: string, + rootDir: string, + mirror: ProductDocsMirror, +) => { + for (const entry of readdirSync(srcDir)) { + const srcPath = join(srcDir, entry); + const destPath = join(destDir, entry); + + if (statSync(srcPath).isDirectory()) { + mkdirSync(destPath, { recursive: true }); + mirrorTree(srcPath, destPath, rootDir, mirror); + continue; + } + + const relPath = relative(rootDir, srcPath); + + // the section-root meta.json is handled separately to inject the title + if (entry === "meta.json" && srcDir === rootDir) continue; + + if (entry.endsWith(".mdx")) { + writeFileSync( + destPath, + transformMdx(readFileSync(srcPath, "utf8"), relPath, mirror), + ); + } else if (entry === "meta.json") { + cpSync(srcPath, destPath); + } + } +}; + +const syncMirror = (mirror: ProductDocsMirror) => { + const destDir = join(contentRoot, mirror.id); + + let checkoutDir: string | undefined; + try { + checkoutDir = checkout(mirror); + } catch (error) { + warn( + `could not fetch ${mirror.repo}@${mirror.ref.slice(0, 8)} (${error}); keeping committed mirror for "${mirror.id}"`, + ); + return; + } + + try { + const srcRoot = join(checkoutDir, mirror.contentDir); + + // rebuild the mirror from scratch so removed upstream pages disappear + rmSync(destDir, { recursive: true, force: true }); + mkdirSync(destDir, { recursive: true }); + + mirrorTree(srcRoot, destDir, srcRoot, mirror); + + const rootMetaPath = join(srcRoot, "meta.json"); + let rootMeta: string | undefined; + try { + rootMeta = readFileSync(rootMetaPath, "utf8"); + } catch { + rootMeta = undefined; + } + writeMeta(rootMeta, destDir, mirror.title); + + // provenance marker for the generated folder; kept stable (no timestamp) so + // the committed mirror does not churn on every build-time sync + writeFileSync( + join(destDir, ".mirror.json"), + `${JSON.stringify({ repo: mirror.repo, ref: mirror.ref }, null, 2)}\n`, + ); + + log(`mirrored "${mirror.id}" from ${mirror.ref.slice(0, 8)}`); + } finally { + rmSync(checkoutDir, { recursive: true, force: true }); + } +}; + +for (const mirror of productDocsMirrors) syncMirror(mirror); diff --git a/source.config.ts b/source.config.ts index be8d1bd..09ccd30 100644 --- a/source.config.ts +++ b/source.config.ts @@ -1,4 +1,9 @@ -import { defineConfig, defineDocs } from "fumadocs-mdx/config"; +import { + defineConfig, + defineDocs, + frontmatterSchema, +} from "fumadocs-mdx/config"; +import { z } from "zod"; /** * Docs configuration. @@ -7,6 +12,15 @@ import { defineConfig, defineDocs } from "fumadocs-mdx/config"; export const docs = defineDocs({ dir: "content/docs", docs: { + // these are set by the product-docs mirror (see `scripts/syncProductDocs.ts`) + // on aggregated pages: `canonical` points back at the product's own docs + // domain, `sourceUrl` links "view source" to the upstream file, and + // `mirroredFrom` records provenance + schema: frontmatterSchema.extend({ + canonical: z.string().optional(), + mirroredFrom: z.string().optional(), + sourceUrl: z.string().optional(), + }), postprocess: { includeProcessedMarkdown: true, }, diff --git a/src/routes/$.tsx b/src/routes/$.tsx index bccf66d..22d083f 100644 --- a/src/routes/$.tsx +++ b/src/routes/$.tsx @@ -27,7 +27,6 @@ import { SidebarSeparator, } from "@/components/layout"; import { LLMCopyButton, ViewOptions } from "@/components/llm"; -import { Button } from "@/components/ui/button"; import { app } from "@/lib/config"; import { useSidebarEscClose } from "@/lib/hooks/useSidebarEscClose"; import { useSidebarScrollPersistence } from "@/lib/hooks/useSidebarScrollPersistence"; @@ -121,24 +120,6 @@ const Page = () => { sidebar={{ banner: (
- - We turn ideas into products, from apps to websites to email - automation. - - - - -