From 6f1a2103b2debbebcaeb893eecccff0428aa2ca6 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:39:01 +0200 Subject: [PATCH] refactor: home web timeline (#2870) * refactor: home timeline * wip: * fix: * fix: timeline --- apps/web/AGENTS.md | 5 +- apps/web/src/content/convert.ts | 2 +- .../src/content/mdx-components/eyebrow.tsx | 29 +++- apps/web/src/content/mdx-components/grid.tsx | 2 + apps/web/src/content/mdx-components/index.tsx | 2 + .../src/content/mdx-components/timeline.tsx | 16 ++ apps/web/src/content/pages/home.mdx | 153 ++++++------------ .../src/content/pages/product/status-page.mdx | 101 +++++++----- .../content/pages/unrelated/kitchen-sink.mdx | 52 ++++++ apps/web/src/styles/globals.css | 16 +- 10 files changed, 229 insertions(+), 149 deletions(-) create mode 100644 apps/web/src/content/mdx-components/timeline.tsx diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 4f4b31133..7d0b8adcb 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -26,7 +26,10 @@ donates domain authority and leaks the conversion. The home and product pages (`pages/home.mdx`, `pages/product/*.mdx`) follow one section pattern: an `h2` outside the grid, a text cell with two sentences and a short list of internal links, and one `` in the other cell of -a ``. `content-lint.test.ts` enforces the structural +a ``. Inside a `` (the home page story) +the `Eyebrow` and `h2` move into the text cell instead, and the text cell comes +first, so the label, heading and copy stick together beside the demo and every +marker lands on the rail. `content-lint.test.ts` enforces the structural rules on those pages (registered tags, `SrOnly` next to every demo, no raw `className`, every demo type in the kitchen sink); the rest of the rules below are kept by hand and in review. `/kitchen-sink` (noindex) renders every diff --git a/apps/web/src/content/convert.ts b/apps/web/src/content/convert.ts index a95c7774c..bb793199a 100644 --- a/apps/web/src/content/convert.ts +++ b/apps/web/src/content/convert.ts @@ -171,7 +171,7 @@ export function convertMdxToMarkdown(data: MDXData): string { markdown = markdown.replace(/]*>[\s\S]*?<\/Eyebrow>/g, ""); // SrOnly is a demo's text alternative: hidden on the page, plain copy here. // One pass skips wrappers nested inside a match, so repeat until stable. - const wrapper = /<(Grid|Subtle|p|SrOnly)\b[^>]*>([\s\S]*?)<\/\1>/g; + const wrapper = /<(Grid|Subtle|p|SrOnly|Timeline)\b[^>]*>([\s\S]*?)<\/\1>/g; while (wrapper.test(markdown)) { markdown = markdown.replace(wrapper, (_match, _tag, content) => content); } diff --git a/apps/web/src/content/mdx-components/eyebrow.tsx b/apps/web/src/content/mdx-components/eyebrow.tsx index a7a4d42a8..a214da3a5 100644 --- a/apps/web/src/content/mdx-components/eyebrow.tsx +++ b/apps/web/src/content/mdx-components/eyebrow.tsx @@ -1,12 +1,33 @@ +import { type VariantProps, cva } from "class-variance-authority"; import type React from "react"; import { cn } from "@/lib/utils"; +const markerVariants = cva( + "border-muted-foreground bg-background absolute top-0.5 -left-6 hidden size-[11px] border in-data-[slot=timeline]:block md:-left-10", + { + variants: { + status: { + investigating: "border-destructive bg-destructive", + identified: "border-warning bg-warning", + monitoring: "border-info bg-info", + resolved: "border-success bg-success", + }, + }, + }, +); + /** * Small label on the line before a heading (a timestamp, a step, a category); * `globals.css` hands it the heading's top rule. Styled like `CellLabel`. + * Inside a `Timeline` it renders a rail marker colored by `status`. */ -export function Eyebrow({ className, ...props }: React.ComponentProps<"div">) { +export function Eyebrow({ + status, + className, + children, + ...props +}: React.ComponentProps<"div"> & VariantProps) { return (
) { className, )} {...props} - /> + > + {/* `cn` lets the status colors override the hollow default. */} + + {children} +
); } diff --git a/apps/web/src/content/mdx-components/grid.tsx b/apps/web/src/content/mdx-components/grid.tsx index 030bf1ba9..acad7f62d 100644 --- a/apps/web/src/content/mdx-components/grid.tsx +++ b/apps/web/src/content/mdx-components/grid.tsx @@ -59,6 +59,8 @@ export function Grid({ // A demo with an unbreakable string must truncate, never widen the page. "[&>*]:min-w-0", "[&>*>*:first-child]:!mt-0 [&>*>*:last-child]:!mb-0", + // The shorter cell follows the taller one down; equal heights don't move. + "md:[&>*]:sticky md:[&>*]:top-8", sm && smColsClass[sm], colsClass[cols], className, diff --git a/apps/web/src/content/mdx-components/index.tsx b/apps/web/src/content/mdx-components/index.tsx index 28d8f8d48..43bc521ca 100644 --- a/apps/web/src/content/mdx-components/index.tsx +++ b/apps/web/src/content/mdx-components/index.tsx @@ -21,6 +21,7 @@ import { SrOnly } from "./sr-only"; import { MDXStatusPageExample } from "./status-page-example"; import { Subtle } from "./subtle"; import { Table } from "./table"; +import { Timeline } from "./timeline"; import { ShowcaseYouTube } from "./youtube"; export { slugify } from "./heading"; @@ -56,5 +57,6 @@ export const components = { PricingTabs, Subtle, Eyebrow, + Timeline, Suspense: Suspense, }; diff --git a/apps/web/src/content/mdx-components/timeline.tsx b/apps/web/src/content/mdx-components/timeline.tsx new file mode 100644 index 000000000..d7c0a441b --- /dev/null +++ b/apps/web/src/content/mdx-components/timeline.tsx @@ -0,0 +1,16 @@ +import type React from "react"; + +/** + * Wraps `Eyebrow` + `h2` + `Grid` steps in a vertical rail; each `Eyebrow` + * becomes a marker on it. `globals.css` drops the rules between steps. + */ +export function Timeline({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/web/src/content/pages/home.mdx b/apps/web/src/content/pages/home.mdx index d3511d9ff..1f9e65dc1 100644 --- a/apps/web/src/content/pages/home.mdx +++ b/apps/web/src/content/pages/home.mdx @@ -44,13 +44,15 @@ A live status page for the fictional Pied Piper, mid-incident: a Degraded Perfor At 09:41 the Checkout API at Pied Piper starts returning 503s from Europe. Here is what openstatus does before the team has finished reading the first alert, and what is left behind six months later. -09:41 - -## Your monitor catches it first +
+09:41 + +## Your monitor catches it first + Four of the six regions the monitor runs from confirm the 503s, so the alert fires before the first support ticket arrives. It lands in Slack and PagerDuty with a link to the failing checks. - [Uptime monitoring](/uptime-monitoring) @@ -71,39 +73,13 @@ The Slack alert at 09:41: Checkout API returned 503 from lhr, ams, cdg and koyeb
-09:42 - -## Know what to say before you say it -
- - - - -Response logs for the 09:41 check, one row per region: the four European regions returned 503 in about 4 seconds, almost all of it TTFB, while Virginia and San Jose returned 200 in about 230 ms. - - - -
-
- -Open the failing checks. The European regions spend four seconds waiting on the edge while DNS and TLS are fine, and the US regions return 200 in under 250 ms. That is the sentence the status report needs, and the headers and body are kept in case the postmortem needs more. - -- [Response logs and retention](/docs/concept/response-logs-and-retention) -- [Request phases: DNS, TCP, TLS, TTFB](/docs/concept/latency-vs-response-time#the-request-in-phases-dns-tcp-tls-ttfb-transfer) - -
-
- -09:43 +09:43 ## Declare the incident, get a channel - -
- One slash command declares the incident with a title and a severity. Openstatus opens a dedicated Slack channel, invites you and pins the incident card, so the response has one place to happen. - [Incident management](/incident-management) @@ -123,23 +99,12 @@ Gilfoyle runs `/openstatus incident declare Checkout API 503s in EU --sev major`
-09:44 - -## Tell customers from Slack, or with your favorite agent -
- - - - -An engineer asks @openstatus in the incident thread to open a status report. The agent drafts "Elevated errors on Checkout API", the engineer approves, and 1,337 subscribers are notified. - - +09:44 -
-
+## Tell customers from Slack, or with your favorite agent Ask the openstatus agent in the incident thread. It drafts the status report, you approve it, and it goes live without anyone opening the dashboard. The same agent runs wherever you work: connect the MCP server to Claude, ChatGPT or Cursor and publish it from there. @@ -149,146 +114,124 @@ Ask the openstatus agent in the incident thread. It drafts the status report, yo - [API](/tooling/api)
-
- -09:52 - -## Everyone hears it from you first - -
-Subscribers get the update by email, RSS and Slack Connect the moment it is approved. Support stops answering "is it down?" because the answer is already in the inbox. - -- [Subscriptions](/docs/reference/subscriber) -- [Reducing support tickets](/use-case/reduce-support-tickets) - -
-
- - + -The 09:52 Identified update as it lands in a customer's Slack Connect channel, with the delivery summary: 1,337 emails sent, RSS and Atom updated, 3 Slack Connect workspaces notified. +An engineer asks @openstatus in the incident thread to open a status report. The agent drafts "Elevated errors on Checkout API", the engineer approves, and 1,337 subscribers are notified.
-09:53 - -## Only the right people see it -
- +10:14 - +## Mitigate in the channel, update the page -Two pages side by side: the public status.piedpiper.dev shows Checkout API degraded, while internal.piedpiper.dev asks for a password and only admits the 203.0.113.0/24 range. +The rollback lands and one slash command marks the incident mitigated. The linked status report moves to monitoring with its own public wording, and subscribers hear about it without anyone leaving Slack. - +- [Incident management](/incident-management) +- [Status page](/status-page)
-The public page shows the Checkout API as degraded. The internal page, behind a password and the office IP range, shows the failing regions and the rollback progress. + -- [Status page](/status-page) -- [Password, magic link and IP restriction](/docs/reference/status-page#password-basic-auth) -- [Status pages for enterprise sales](/use-case/enterprise-sales) + -
-
+At 10:14 Gilfoyle runs `/openstatus incident mitigate Rollback complete in eu-west-1 and eu-west-2.` in the incident channel and the app confirms the incident is now mitigated. Below it, the linked status report "Elevated errors on Checkout API" reads Monitoring, with its own public message and 1,337 subscribers notified. -10:14 + -## Update the status where the work happens + +
-When the rollback lands, mark the incident mitigated from its channel. The internal status and the public status report move separately, so the team says "mitigated" while customers read "monitoring". +11:02 -- [Incident management](/incident-management#internal-status-public-update) -- [Status report reference](/docs/reference/status-report) +## The postmortem arrives as a draft + +Once the incident is resolved, the agent drafts a blameless postmortem from the timeline, the public updates and the channel history. You edit it, approve it and close the incident while everyone still remembers what happened. + +- [Incident management](/incident-management#a-postmortem-with-a-first-draft) +- [MCP server](/tooling/mcp-server)
- + -At 10:14 Gilfoyle runs `/openstatus incident mitigate Rollback complete in eu-west-1 and eu-west-2.` in the incident channel and the app confirms the incident is now mitigated. The linked status report "Elevated errors on Checkout API" reads Monitoring, with 1,337 subscribers notified. +The postmortem draft, written by the agent at 11:02: summary, impact (55m, from 09:41 to 10:36 UTC, Europe only), a UTC timeline, root cause (the 09:38 edge config deploy), what went well, what went wrong and two action items, with buttons to approve it or draft again.
-11:02 - -## The postmortem arrives as a draft -
- ++1 week - +## The record stays public -The postmortem draft, written by the agent at 11:02: summary, impact (55m, from 09:41 to 10:36 UTC, Europe only), a UTC timeline, root cause (the 09:38 edge config deploy), what went well, what went wrong and two action items, with buttons to approve it or draft again. +Every update stays on the status page with its timestamp. When a prospect asks how you handle outages, the answer is already in your incident history. - +- [Status page](/status-page) +- [Status pages for enterprise sales](/use-case/enterprise-sales)
-Once the incident is resolved, the agent drafts a blameless postmortem from the timeline, the public updates and the channel history. You edit it, approve it and close the incident while everyone still remembers what happened. + -- [Incident management](/incident-management#a-postmortem-with-a-first-draft) -- [MCP server](/tooling/mcp-server) + + +The same incident a week later in the status page's events feed: four timestamped updates from investigating at 09:44 to resolved at 10:36. + +
+ +
+ +6 months ## The trail exists when the auditor asks - -
- Every step is logged: who declared it, from where, when each update shipped. When SOC 2 asks how you notify customers during an incident, you send one link. - [Status pages for compliance](/use-case/compliance) - - - - -The incident's audit log, with the newest entry at the top. From oldest to newest: monitor.alert at 09:41:12, incident.create at 09:43:08 and status_report.create at 09:44:30 by gilfoyle@piedpiper.dev via Slack, notification.send a second later, then the identified, monitoring and resolved updates, the incident mitigated and resolved, and the postmortem approved at 11:20:48. It exports as CSV or JSON. - - -
- + -The same incident six months later in the status page's events feed: four timestamped updates from investigating at 09:44 to resolved at 10:36. +The incident's audit log, with the newest entry at the top. From oldest to newest: monitor.alert at 09:41:12, incident.create at 09:43:08 and status_report.create at 09:44:30 by gilfoyle@piedpiper.dev via Slack, notification.send a second later, then the identified, monitoring and resolved updates, the incident mitigated and resolved, and the postmortem approved at 11:20:48. It exports as CSV or JSON.
+ + ## In their words diff --git a/apps/web/src/content/pages/product/status-page.mdx b/apps/web/src/content/pages/product/status-page.mdx index 91ace0af1..c8922c020 100644 --- a/apps/web/src/content/pages/product/status-page.mdx +++ b/apps/web/src/content/pages/product/status-page.mdx @@ -127,163 +127,178 @@ Users subscribe once and get every status report and maintenance by email, RSS,
-## Translations +## Everyone hears it from you first
-Set a default locale and turn on the switcher. The page chrome and the canonical status copy are translated; your updates stay in the language you wrote them. - -English, French, German, Turkish, Hindi, Korean and Japanese today, with more from community contributions. +The moment an update is approved, subscribers get it by email, RSS and Slack Connect. Support stops answering "is it down?" because the answer is already in the inbox. -- [Translate your status page](/docs/guides/how-to-translate-status-page) +- [Reducing support tickets](/use-case/reduce-support-tickets)
- + -The same page in French: a "Performances dégradées" banner, components labelled Opérationnel or Dégradé, and a switcher offering English, Deutsch, Français and 日本語. +The 09:52 Identified update as it lands in a customer's Slack Connect channel, with the delivery summary: 1,337 emails sent, RSS and Atom updated, 3 Slack Connect workspaces notified.
-## Slack agent +## Translations
- + -The Slack thread where @openstatus drafts "Elevated errors on Checkout API" with Approve, Approve & notify and Cancel buttons, then confirms it is live on status.piedpiper.dev. +The same page in French: a "Performances dégradées" banner, components labelled Opérationnel or Dégradé, and a switcher offering English, Deutsch, Français and 日本語.
-Mention @openstatus in any thread. It drafts the report, you approve, it publishes. Follow up in the same thread with "it's fixed" and it drafts the next update. +Set a default locale and turn on the switcher. The page chrome and the canonical status copy are translated; your updates stay in the language you wrote them. -Every action asks for confirmation before anything goes public, and every action is written to the audit log. +English, French, German, Turkish, Hindi, Korean and Japanese today, with more from community contributions. -- [Set up the Slack agent](/docs/guides/how-to-setup-slack-agent) +- [Translate your status page](/docs/guides/how-to-translate-status-page)
-## Branded, on your domain +## Slack agent
-Pick a theme from the store or override any CSS variable, separately for light and dark. Point status.yourdomain.com at it, and even white label it to drop the "Powered by" line. +Mention @openstatus in any thread. It drafts the report, you approve, it publishes. Follow up in the same thread with "it's fixed" and it drafts the next update. -- [Theme Store](https://themes.openstatus.dev) -- [Custom theme variables](/docs/reference/status-page#custom-theme) +Every action asks for confirmation before anything goes public, and every action is written to the audit log. + +- [Set up the Slack agent](/docs/guides/how-to-setup-slack-agent)
- + -The same status blocks re-skinned with a Theme Store theme: banner, component and uptime bar follow its CSS variables such as --success, --warning and --radius, in light and dark. +The Slack thread where @openstatus drafts "Elevated errors on Checkout API" with Approve, Approve & notify and Cancel buttons, then confirms it is live on status.piedpiper.dev.
-## Internal and private pages +## Branded, on your domain
- + -Public and internal pages side by side: the public one lists all four components, the internal one shows a password prompt with an IP allowlist of 203.0.113.0/24 switched on. +The same status blocks re-skinned with a Theme Store theme: banner, component and uptime bar follow its CSS variables such as --success, --warning and --radius, in light and dark.
-Public by default. Protect a page with a password, magic-link login, or a CIDR allowlist so internal teams and named clients see more than the public does. +Pick a theme from the store or override any CSS variable, separately for light and dark. Point status.yourdomain.com at it, and even white label it to drop the "Powered by" line. -- [Password, magic link and IP restriction](/docs/reference/status-page#password-basic-auth) -- [Status pages for enterprise sales](/use-case/enterprise-sales) +- [Theme Store](https://themes.openstatus.dev) +- [Custom theme variables](/docs/reference/status-page#custom-theme)
-## Maintenance windows +## Internal and private pages
-Schedule the window ahead of time. Subscribers are told when it is planned, the banner flips to maintenance while it runs, and affected components stop counting against uptime. +Public by default. Protect a page with a password, magic-link login, or a CIDR allowlist so internal teams and named clients see more than the public does. -- [Maintenance reference](/docs/reference/maintenance) +- [Password, magic link and IP restriction](/docs/reference/status-page#password-basic-auth) +- [Status pages for enterprise sales](/use-case/enterprise-sales)
- + -A scheduled maintenance banner, "Database upgrade": a two-hour window three days out affecting Checkout API and Webhooks, with 1,337 subscribers notified when it was scheduled. +Public and internal pages side by side: the public one lists all four components, the internal one shows a password prompt with an IP allowlist of 203.0.113.0/24 switched on.
-## Import from another provider +## Maintenance windows
- + -The import preview for Atlassian Statuspage: 5 components, 2 groups, 37 status reports, 3 maintenances, 1,337 subscribers and 4 monitors, shown before anything is written. +A scheduled maintenance banner, "Database upgrade": a two-hour window three days out affecting Checkout API and Webhooks, with 1,337 subscribers notified when it was scheduled.
-Paste an API key from Atlassian Statuspage, Better Stack or Instatus. Preview components, incidents, maintenances and subscribers before anything is written. +Schedule the window ahead of time. Subscribers are told when it is planned, the banner flips to maintenance while it runs, and affected components stop counting against uptime. -- [Import guide](/docs/guides/how-to-import-status-page) +- [Maintenance reference](/docs/reference/maintenance)
-## For terminals and agents +## Import from another provider
-The same page over SSH for people in a terminal, and as markdown for the agents and LLMs your customers point at it. No screenshots, no scraping. +Paste an API key from Atlassian Statuspage, Better Stack or Instatus. Preview components, incidents, maintenances and subscribers before anything is written. -- [SSH command](/docs/reference/status-page#ssh-command) -- [Shadcn component registry](/registry) +- [Import guide](/docs/guides/how-to-import-status-page)
+ + + + +The import preview for Atlassian Statuspage: 5 components, 2 groups, 37 status reports, 3 maintenances, 1,337 subscribers and 4 monitors, shown before anything is written. + + + +
+
+ +## For terminals and agents + + +
+ @@ -292,6 +307,14 @@ The page over SSH, `ssh pied-piper@ssh.openstatus.dev`, printing "Degraded Perfo +
+
+ +The same page over SSH for people in a terminal, and as markdown for the agents and LLMs your customers point at it. No screenshots, no scraping. + +- [SSH command](/docs/reference/status-page#ssh-command) +- [Shadcn component registry](/registry) +
diff --git a/apps/web/src/content/pages/unrelated/kitchen-sink.mdx b/apps/web/src/content/pages/unrelated/kitchen-sink.mdx index 03d0a9f41..df8ee9aae 100644 --- a/apps/web/src/content/pages/unrelated/kitchen-sink.mdx +++ b/apps/web/src/content/pages/unrelated/kitchen-sink.mdx @@ -232,6 +232,58 @@ What the demo shows, for screen readers and the `.md` representation of the page
+## Timeline + + + + +
+ +09:41 + +### A step on the rail + +The eyebrow and heading open the text cell, so they stick with it. The marker takes the status-report color of the moment it describes. + +
+
+ + + + + +The Slack alert at 09:41 for the failing Checkout API. + + + +
+
+ + +
+ ++6 months + +### A step without a status + +No `status` renders a hollow marker. + +
+
+ + + + + +The incident's audit log. + + + +
+
+ +
+ ## LogoCloud diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index 1379a2254..354ea7716 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -212,10 +212,24 @@ @apply mt-12 mb-2 pt-8 border-t border-border/60; } -.prose [data-slot="eyebrow"] + h2 { +.prose [data-slot="eyebrow"] + :is(h2, h3) { @apply mt-0 pt-0 border-t-0; } +/* In a `Timeline` each step is a grid whose text cell opens with the eyebrow + and heading, so they stick with it; the rail replaces the rule. */ +.prose [data-slot="timeline"] [data-slot="eyebrow"] { + @apply relative mt-0 pt-0 border-t-0; +} + +.prose [data-slot="timeline"] h2 { + @apply text-balance; +} + +.prose [data-slot="timeline"] > * + * { + @apply mt-20; +} + .prose h3 { @apply text-xl text-foreground font-medium tracking-tight mt-8 mb-3 relative scroll-mt-8; } -- 2.51.2