diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index eabf4f0e..935fc37e 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -12,7 +12,7 @@ const securityHeaders = [ /** @type {import('next').NextConfig} */ const nextConfig: NextConfig = { reactStrictMode: true, - transpilePackages: ["@openstatus/ui", "@openstatus/api"], + transpilePackages: ["@openstatus/ui", "@openstatus/api", "next-mdx-remote"], outputFileTracingIncludes: { "/": [ "./node_modules/.pnpm/@google-cloud/tasks/build/esm/src/**/*.json", @@ -216,6 +216,18 @@ const nextConfig: NextConfig = { ], destination: "https://www.stpg.dev/_next/:path*", }, + // Markdown content negotiation for AI tools + { + source: "/:path*", + destination: "/api/markdown/:path*", + has: [ + { + type: "header", + key: "accept", + value: ".*text/markdown.*", + }, + ], + }, ], }; }, diff --git a/apps/web/package.json b/apps/web/package.json index 42a82b78..fae0a9d4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -83,6 +83,7 @@ "react-tweet": "3.2.1", "reading-time": "1.5.0", "recharts": "2.15.0", + "remark-gfm": "4.0.1", "resend": "6.6.0", "sanitize-html": "2.17.0", "schema-dts": "1.1.5", diff --git a/apps/web/src/app/api/markdown/[...path]/route.ts b/apps/web/src/app/api/markdown/[...path]/route.ts new file mode 100644 index 00000000..eacb9e58 --- /dev/null +++ b/apps/web/src/app/api/markdown/[...path]/route.ts @@ -0,0 +1,68 @@ +import { convertMdxToMarkdown } from "@/content/convert"; +import { resolveContent } from "@/content/resolve"; +import { type NextRequest, NextResponse } from "next/server"; + +export const runtime = "nodejs"; // Need fs access for content loading + +/** + * GET handler for markdown content negotiation + * Serves clean markdown when Accept: text/markdown header is present + */ +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> }, +) { + try { + // Extract pathname from catch-all params + const { path: pathSegments } = await params; + const pathname = pathSegments ? `/${pathSegments.join("/")}` : "/"; + + // Resolve content (MDX or listing) + const result = resolveContent(pathname); + + if (!result) { + return new NextResponse("Not Found", { status: 404 }); + } + + let markdown: string; + let contentSource: string; + + // Handle based on content type + if (result.type === "mdx") { + // Convert MDX to markdown with metadata for frontmatter + markdown = convertMdxToMarkdown(result.data); + contentSource = "mdx"; + } else { + // Use pre-generated listing + markdown = result.data; + contentSource = "listing"; + } + + // Return with appropriate headers + return new NextResponse(markdown, { + status: 200, + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": + "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400", + "X-Content-Source": contentSource, + }, + }); + } catch (error) { + console.error("Error serving markdown:", error); + return new NextResponse("Internal Server Error", { status: 500 }); + } +} + +// Only allow GET requests +export async function POST() { + return new NextResponse("Method Not Allowed", { status: 405 }); +} + +export async function PUT() { + return new NextResponse("Method Not Allowed", { status: 405 }); +} + +export async function DELETE() { + return new NextResponse("Method Not Allowed", { status: 405 }); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ace0d12f..c871e0d1 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -82,6 +82,7 @@ export default function RootLayout({ info: null, loading: null, }} + richColors /> diff --git a/apps/web/src/content/convert.ts b/apps/web/src/content/convert.ts new file mode 100644 index 00000000..be439232 --- /dev/null +++ b/apps/web/src/content/convert.ts @@ -0,0 +1,122 @@ +import type { MDXData } from "./utils"; +import { formatDate } from "./utils"; + +/** + * Converts MDX content to clean markdown format for AI tools + * Handles YAML frontmatter generation and component serialization + */ +export function convertMdxToMarkdown(data: MDXData): string { + let output = ""; + + // Step 0: Generate YAML frontmatter from metadata + const { metadata, content } = data; + + output += "---\n"; + output += `title: "${metadata.title}"\n`; + if (metadata.publishedAt) { + output += `date: ${formatDate(metadata.publishedAt)}\n`; + } + if (metadata.author) { + output += `author: "${metadata.author}"\n`; + } + if (metadata.description) { + output += `description: "${metadata.description}"\n`; + } + if (metadata.category) { + output += `category: "${metadata.category}"\n`; + } + if (metadata.image) { + output += `image: "${metadata.image}"\n`; + } + output += "---\n\n"; + + // Step 1: Strip import statements (confirmed: always at top of file) + let markdown = content.replace(/^import\s+.*$/gm, ""); + + // Step 2: Convert to markdown links + markdown = markdown.replace( + /]*>([\s\S]*?)<\/ButtonLink>/g, + (_match, href, children) => { + // Extract plain text from children (remove any nested tags) + const text = children.replace(/<[^>]*>/g, "").trim(); + return `[${text}](${href})`; + }, + ); + + // Step 3: Convert to markdown images + // Handle with alt attribute + markdown = markdown.replace( + /]*\s+)?src="([^"]*)"(?:[^>]*\s+)?alt="([^"]*)"[^>]*\/?>/g, + (_match, src, alt) => { + return `![${alt}](${src})`; + }, + ); + // Handle without alt attribute + markdown = markdown.replace( + /]*\s+)?src="([^"]*)"[^>]*\/?>/g, + (_match, src) => { + return `![](${src})`; + }, + ); + + // Step 4: Replace components with a note to use GFM tables + // TODO: Convert Table components in MDX source files to GFM tables + markdown = markdown.replace(//g, () => { + return ""; + }); + + // Step 5: Convert
to native HTML
/ (always closed) + markdown = markdown.replace( + /]*>([\s\S]*?)<\/Details>/g, + (_match, summary, content) => { + return `
\n ${summary}\n ${content.trim()}\n
`; + }, + ); + + // Step 6: Replace interactive components with HTML comments + markdown = markdown.replace( + /]*(?:\/>|>[\s\S]*?<\/Tweet>)/g, + "", + ); + markdown = markdown.replace( + /]*(?:\/>|>[\s\S]*?<\/StatusPageExample>)/g, + "", + ); + markdown = markdown.replace( + /]*(?:\/>|>[\s\S]*?<\/SimpleChart>)/g, + "", + ); + + // Step 7: Extract text from containers (remove Grid wrapper, keep content) + markdown = markdown.replace( + /]*>([\s\S]*?)<\/Grid>/g, + (_match, content) => { + return content; + }, + ); + + // Step 8: Strip generic HTML containers but extract their text + // This handles div, span, section, article + markdown = markdown.replace( + /<(div|span|section|article)(?:\s+[^>]*)?>[\s\S]*?<\/\1>/g, + (match) => { + // Extract text content (remove all tags) + return match.replace(/<[^>]*>/g, " ").trim(); + }, + ); + + // Step 9: Strip remaining unknown JSX components + // Handle paired tags + markdown = markdown.replace(/<[A-Z]\w*[^>]*>[\s\S]*?<\/[A-Z]\w*>/g, ""); + // Handle self-closing tags + markdown = markdown.replace(/<[A-Z]\w*\s*\/>/g, ""); + + // Clean up: Remove excessive blank lines (more than 2 consecutive) + markdown = markdown.replace(/\n{3,}/g, "\n\n"); + + // Trim whitespace + markdown = markdown.trim(); + + output += markdown; + return output; +} diff --git a/apps/web/src/content/copy-button.tsx b/apps/web/src/content/copy-button.tsx index 851b5860..7ea6e66f 100644 --- a/apps/web/src/content/copy-button.tsx +++ b/apps/web/src/content/copy-button.tsx @@ -1,8 +1,17 @@ "use client"; import { Button } from "@openstatus/ui/components/ui/button"; +import { ButtonGroup } from "@openstatus/ui/components/ui/button-group"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@openstatus/ui/components/ui/dropdown-menu"; import { useCopyToClipboard } from "@openstatus/ui/hooks/use-copy-to-clipboard"; import { cn } from "@openstatus/ui/lib/utils"; +import { toast } from "sonner"; export function CopyButton({ className, @@ -29,3 +38,79 @@ export function CopyButton({ ); } + +export function CopyDropdownButton({ + className, + ...props +}: React.ComponentProps) { + const { copy, isCopied } = useCopyToClipboard(); + + const handleCopyLink = () => { + copy(window.location.href, { + successMessage: "Link copied to clipboard", + withToast: true, + }); + }; + + const handleCopyMarkdown = async () => { + try { + const item = new ClipboardItem({ + "text/plain": fetch(window.location.pathname, { + headers: { Accept: "text/markdown" }, + }) + .then((r) => { + if (!r.ok) throw new Error("Failed to fetch markdown"); + return r.text(); + }) + .then((text) => new Blob([text], { type: "text/plain" })), + }); + + await navigator.clipboard.write([item]); + toast.success("Markdown copied to clipboard"); + } catch (_error) { + toast.error("Failed to copy markdown"); + } + }; + + return ( + + + + + + + + + + [copy markdown] + + + + + + ); +} diff --git a/apps/web/src/content/listing.ts b/apps/web/src/content/listing.ts new file mode 100644 index 00000000..ccbc28ba --- /dev/null +++ b/apps/web/src/content/listing.ts @@ -0,0 +1,130 @@ +import { + type MDXData, + formatDate, + getBlogPosts, + getChangelogPosts, + getComparePages, + getGuides, +} from "./utils"; + +/** + * Generates sitemap-style markdown listings for index and category pages + * Used as fallback when MDX content is not found + */ +export function generateListingForPath(pathname: string): string | null { + const segments = pathname.split("/").filter(Boolean); + + // Root path → overview of all content + if (segments.length === 0) { + return generateRootListing(); + } + + const [category, subcategory, slug] = segments; + + // Index pages: /blog, /changelog, etc. + if (!subcategory) { + switch (category) { + case "blog": + return generatePostsList(getBlogPosts(), "Blog Posts"); + case "changelog": + return generatePostsList(getChangelogPosts(), "Changelog"); + case "compare": + return generatePostsList(getComparePages(), "Comparisons"); + case "guides": + return generatePostsList(getGuides(), "Guides"); + default: + return null; + } + } + + // Category pages: /blog/category/engineering + if (subcategory === "category" && slug) { + return generateCategoryList(category, slug); + } + + return null; +} + +/** + * Generate a markdown list from posts + */ +function generatePostsList(posts: MDXData[], title: string): string { + const sorted = posts.sort( + (a, b) => + b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(), + ); + + const items = sorted + .map( + (post) => + `- [${post.metadata.title}](${post.href}) - ${formatDate(post.metadata.publishedAt)}`, + ) + .join("\n"); + + return `# ${title}\n\n${items}\n`; +} + +/** + * Generate category-filtered listings + */ +function generateCategoryList(type: string, category: string): string | null { + let posts: MDXData[]; + let title: string; + + switch (type) { + case "blog": + posts = getBlogPosts(); + title = `Blog Posts - ${category}`; + break; + case "changelog": + posts = getChangelogPosts(); + title = `Changelog - ${category}`; + break; + default: + return null; + } + + const filtered = posts.filter((p) => p.metadata.category === category); + + if (filtered.length === 0) { + return null; + } + + return generatePostsList(filtered, title); +} + +/** + * Generate root listing with overview of all content sections + */ +function generateRootListing(): string { + const sections = [ + { title: "Blog", posts: getBlogPosts(), path: "/blog" }, + { title: "Changelog", posts: getChangelogPosts(), path: "/changelog" }, + { title: "Comparisons", posts: getComparePages(), path: "/compare" }, + { title: "Guides", posts: getGuides(), path: "/guides" }, + ]; + + const content = sections + .map((section) => { + const count = section.posts.length; + const sortedPosts = section.posts.sort( + (a, b) => + b.metadata.publishedAt.getTime() - a.metadata.publishedAt.getTime(), + ); + + const topPosts = sortedPosts + .slice(0, 5) + .map((p) => `- [${p.metadata.title}](${p.href})`) + .join("\n"); + + const viewAll = + count > 5 + ? `\n- [View all ${count} ${section.title.toLowerCase()}...](${section.path})` + : ""; + + return `## [${section.title}](${section.path}) (${count})\n\n${topPosts}${viewAll}`; + }) + .join("\n\n"); + + return `# OpenStatus Content\n\n${content}\n`; +} diff --git a/apps/web/src/content/mdx.tsx b/apps/web/src/content/mdx.tsx index 84a276fe..3b745298 100644 --- a/apps/web/src/content/mdx.tsx +++ b/apps/web/src/content/mdx.tsx @@ -8,6 +8,7 @@ import Image from "next/image"; import Link from "next/link"; import React from "react"; import { Tweet, type TweetProps } from "react-tweet"; +import remarkGfm from "remark-gfm"; import { highlight } from "sugar-high"; import { ComponentHighlighter } from "./component-highlighter"; import { CopyButton } from "./copy-button"; @@ -16,30 +17,10 @@ import { ImageZoom } from "./image-zoom"; import { LatencyChartTable } from "./latency-chart-table"; import { StatusPageExample } from "./shadcn-registry-example"; -function Table({ - data, -}: { - data: { headers: React.ReactNode[]; rows: React.ReactNode[][] }; -}) { - const headers = data.headers.map((header: React.ReactNode, index: number) => ( -
- )); - const rows = data.rows.map((row: React.ReactNode[], index: number) => ( - - {row.map((cell: React.ReactNode, cellIndex: number) => ( - - ))} - - )); - +function Table(props: React.ComponentProps<"table">) { return (
-
{header}
{cell}
- - {headers} - - {rows} -
+ ); } @@ -352,7 +333,7 @@ export const components = { ButtonLink: ButtonLink, code: Code, pre: Pre, - Table, + table: Table, Grid, Details, // Capital D for JSX usage with props details: Details, // lowercase for HTML tag replacement @@ -378,6 +359,10 @@ function MDXContent(props: MDXRemoteProps) { options={{ blockJS: false, // Allow JS expressions in trusted MDX content blockDangerousJS: true, // Still block dangerous operations + mdxOptions: { + remarkPlugins: [remarkGfm], + ...props.options?.mdxOptions, + }, ...props.options, }} components={ diff --git a/apps/web/src/content/pages/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render.mdx b/apps/web/src/content/pages/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render.mdx index a0689fe6..1b96ad8c 100644 --- a/apps/web/src/content/pages/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render.mdx +++ b/apps/web/src/content/pages/blog/monitoring-latency-cf-workers-fly-koyeb-raylway-render.mdx @@ -109,19 +109,14 @@ scripts for free, running on more than 275 network locations. ### Timing metrics -
+| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) | +| --- | --- | --- | --- | --- | --- | +| AMS | 17 | 2 | 17 | 27 | 0 | +| GRU | 38 | 2 | 13 | 28 | 0 | +| HKG | 19 | 2 | 13 | 29 | 0 | +| IAD | 24 | 1 | 14 | 30 | 0 | +| JNB | 123 | 168 | 182 | 185 | 0 | +| SYD | 51 | 1 | 11 | 25 | 0 | I can notice that Johannesburg's latency is about ten times higher than that of the other monitors. @@ -131,29 +126,24 @@ the other monitors. From the Cloudflare request I can get the location of the workers that handle the request, with `Cf-ray` in the headers response. -
+| Checker region | Workers region | number of request | +| --- | --- | --- | +| HKG | HKG | 1831 | +| SYD | SYD | 1831 | +| AMS | AMS | 1831 | +| IAD | IAD | 1831 | +| GRU | GRU | 1791 | +| GRU | GIG | 40 | +| JNB | AMS | 741 | +| JNB | MUC | 4 | +| JNB | HKG | 5 | +| JNB | SIN | 6 | +| JNB | NRT | 8 | +| JNB | EWR | 10 | +| JNB | CDG | 82 | +| JNB | FRA | 276 | +| JNB | LHR | 699 | +| JNB | AMS | 741 | I can see all the request from JNB is never routed to a nearby data-center. @@ -225,19 +215,14 @@ Docker images. ### Timing metrics -
+| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) | +| --- | --- | --- | --- | --- | --- | +| AMS | 6 | 1 | 8 | 1469 | 0 | +| GRU | 5 | 0 | 4 | 1431 | 0 | +| HKG | 4 | 0 | 5 | 1473 | 0 | +| IAD | 3 | 0 | 5 | 1470 | 0 | +| JNB | 24 | 0 | 5 | 1423 | 0 | +| SYD | 3 | 0 | 3 | 1489 | 0 | The DNS is fast, our checker is attempting to connect to a region in the same data center, but our machine's cold start is slowing us down, leading to the @@ -417,19 +402,14 @@ developers ### Timing metrics -
+| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) | +| --- | --- | --- | --- | --- | --- | +| AMS | 50 | 2 | 17 | 107 | 0 | +| GRU | 139 | 65 | 75 | 407 | 0 | +| HKG | 48 | 2 | 13 | 321 | 0 | +| IAD | 35 | 1 | 12 | 129 | 0 | +| JNB | 298 | 1 | 11 | 720 | 0 | +| SYD | 97 | 1 | 10 | 711 | 0 | ### Headers @@ -445,42 +425,32 @@ Cf workers -> koyeb Global load balancer -> koyeb backend Let's see where did we hit the cf workers -
+| Checker region | Workers region | number of request | +| --- | --- | --- | +| AMS | AMS | 1866 | +| GRU | GRU | 504 | +| GRU | IAD | 38 | +| GRU | MIA | 688 | +| GRU | EWR | 337 | +| GRU | CIG | 299 | +| HKG | HKG | 1866 | +| IAD | IAD | 1866 | +| JNB | JNB | 1861 | +| JNB | AMS | 1 | +| SYD | SYD | 1866 | Koyeb Global Load Balancer region we hit: -
+| Checker region | Koyeb Global Load Balancer | number of request | +| --- | --- | --- | +| AMS | FRA1 | 1866 | +| GRU | WAS1 | 1866 | +| HKG | SIN1 | 1866 | +| IAD | WAS1 | 1866 | +| JNB | PAR1 | 4 | +| JNB | SIN1 | 1864 | +| JNB | FRA1 | 1 | +| JNB | SIN1 | 1866 | I have deployed our app in the Frankfurt data-center. @@ -547,19 +517,14 @@ capabilities. ### Timing metrics -
+| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) | +| --- | --- | --- | --- | --- | --- | +| AMS | 9 | 21 | 18 | 158 | 0 | +| GRU | 14 | 115 | 127 | 178 | 0 | +| HKG | 8 | 45 | 54 | 225 | 0 | +| IAD | 7 | 2 | 14 | 65 | 0 | +| JNB | 18 | 193 | 178 | 319 | 0 | +| SYD | 21 | 108 | 105 | 280 | 0 | ### Headers @@ -636,19 +601,14 @@ focuses on simplicity and developer productivity. ### Timing metrics -
+| Region | DNS (ms) | Connection (ms) | TLS Handshake (ms) | TTFB (ms) | Transfert (ms) | +| --- | --- | --- | --- | --- | --- | +| AMS | 20 | 2 | 7 | 107 | 0 | +| GRU | 61 | 2 | 6 | 407 | 0 | +| HKG | 76 | 2 | 6 | 321 | 0 | +| IAD | 15 | 1 | 5 | 129 | 0 | +| JNB | 36 | 161 | 167 | 720 | 0 | +| SYD | 103 | 1 | 4 | 711 | 0 | ### Headers @@ -672,18 +632,13 @@ inflection point between cold and warm. Here are the results of our test: -
+| Provider | Uptime | Fails Ping | Total Pings | AVG latency (ms) | P75 (ms) | P90 (ms) | P95 (ms) | P99 (ms) | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| CF Workers | 100 | 0 | 10 | 956 | 182 | 138 | 690 | 778 | +| Fly.io | 100 | 0 | 10 | 952 | 1 | 471 | 1 | 514 | +| Koyeb | 100 | 0 | 10 | 955 | 536 | 738 | 881 | 1 | +| Railway | 99.991 | 1 | 10 | 955 | 381 | 469 | 653 | 661 | +| Render | 99.89 | 12 | 10 | 946 | 451 | 447 | 591 | 707 | If you value low latency, Cloudflare Workers are the best option for fast global performance without cold start issues. They deploy your app worldwide diff --git a/apps/web/src/content/pages/blog/monitoring-latency-vercel-edge-vs-serverless.mdx b/apps/web/src/content/pages/blog/monitoring-latency-vercel-edge-vs-serverless.mdx index 742c4584..b068a9f4 100644 --- a/apps/web/src/content/pages/blog/monitoring-latency-vercel-edge-vs-serverless.mdx +++ b/apps/web/src/content/pages/blog/monitoring-latency-vercel-edge-vs-serverless.mdx @@ -314,16 +314,11 @@ We are pinging this functions every 10 minutes. ## Conclusion -
+| Runtime | p50 | p95 | p99 | +| --- | --- | --- | --- | +| Serverless Cold Start | 859ms | 1 | 046ms | +| Serverless Warm | 246ms | 563ms | 855ms | +| Edge | 106ms | 178ms | 328ms | Globablly Edge functions are approximately 9 times faster than Serverless functions during cold starts, but only 2 times faster when the function is warm. diff --git a/apps/web/src/content/pages/compare/betterstack.mdx b/apps/web/src/content/pages/compare/betterstack.mdx index ed0aa327..f71ee519 100644 --- a/apps/web/src/content/pages/compare/betterstack.mdx +++ b/apps/web/src/content/pages/compare/betterstack.mdx @@ -6,20 +6,15 @@ description: "Open-source uptime monitoring. Learn how OpenStatus compares to Be category: "Company" --- -
+| Feature | openstatus | BetterStack | +| --- | --- | --- | +| Open-source | + | - | +| Bootstrap | + | - | +| Multi-region | 28 | 4 | +| Scheduling strategy | parallel | round-robin | +| Incident escalation | - | + | +| OpenTelemetry exporter | + | - | +| GitHub Action | + | - | +| CLI to trigger checks | + | - | +| Private status page | included in team plan | additional $42/mo. | +| Status page subscribers | Unlimited | additional $42/mo. per 1000 | diff --git a/apps/web/src/content/pages/compare/checkly.mdx b/apps/web/src/content/pages/compare/checkly.mdx index f9f5ed86..d7b5e3d6 100644 --- a/apps/web/src/content/pages/compare/checkly.mdx +++ b/apps/web/src/content/pages/compare/checkly.mdx @@ -6,14 +6,9 @@ description: "Open-source uptime monitoring. Learn how OpenStatus compares to Ch category: "Company" --- -
+| Feature | OpenStatus | Checkly | +| --- | --- | --- | +| Multi-region | 28 | 19 | +| Status Page | + | ? | +| Open Source | + | - | +| Bootstrap | + | - | diff --git a/apps/web/src/content/pages/compare/uptime-kuma.mdx b/apps/web/src/content/pages/compare/uptime-kuma.mdx index d6515e50..51a9ca38 100644 --- a/apps/web/src/content/pages/compare/uptime-kuma.mdx +++ b/apps/web/src/content/pages/compare/uptime-kuma.mdx @@ -6,19 +6,14 @@ description: "Open-source uptime monitoring. Learn how OpenStatus compares to Up category: "Company" --- -
+| Feature | OpenStatus | Uptime Kuma | +| --- | --- | --- | +| Open Source | + | + | +| Self-hosted or cloud-based | + | Self-hosted only | +| Bootstrap | Talk directly to the founder | - | +| Multi-cloud | 3 cloud providers | Single server | +| Global (28 regions) | 28 | 1 | +| Monitor your endpoints globally | + | Single location | +| OTel Export | Export synthetic checks to OTel | - | +| GitHub Action | Trigger via CI/CD | - | +| Team members | Unlimited | Unlimited | diff --git a/apps/web/src/content/pages/compare/uptime-robot.mdx b/apps/web/src/content/pages/compare/uptime-robot.mdx index 6de925df..f7cc3950 100644 --- a/apps/web/src/content/pages/compare/uptime-robot.mdx +++ b/apps/web/src/content/pages/compare/uptime-robot.mdx @@ -6,17 +6,12 @@ description: "Open-source uptime monitoring. Learn how openstatus compares to Up category: "Company" --- -
+| Feature | OpenStatus | UptimeRobot | +| --- | --- | --- | +| Open Source | + | - | +| Bootstrap | + | - | +| Multi-cloud | 3 | ? | +| Multi-region | 28 | - | +| OTel Export | Export synthetic checks to OTel | - | +| GitHub Action | Trigger via CI/CD | - | +| Team members | Unlimited | additional $19/seat | diff --git a/apps/web/src/content/pages/guides/public-vs-private-status-pages.mdx b/apps/web/src/content/pages/guides/public-vs-private-status-pages.mdx index fe9bd26c..f528a6f5 100644 --- a/apps/web/src/content/pages/guides/public-vs-private-status-pages.mdx +++ b/apps/web/src/content/pages/guides/public-vs-private-status-pages.mdx @@ -21,18 +21,13 @@ faq: **TL;DR** - Public status pages face your customers and build trust through transparency. Private status pages face your team and partners, giving them deeper context behind authentication. Once you have more than a handful of engineers or paying enterprise customers, you'll need both. -
Audience, "Customers, end users, the internet", "Internal teams, partners, enterprise clients"], - [Access control, "None - open to anyone", "Password, SSO, IP restriction, email domain"], - [Transparency, "Measured - share what's confirmed", "High - share everything you can"], - [Communication, "Reviewed status reports", "Real-time metrics, auto-incidents, detailed reports"], - [Search engines, "Indexed and discoverable", "Hidden from crawlers"], - ], - }} -/> +| | Public | Private | +| --- | --- | --- | +| **Audience** | Customers | end users | +| **Access control** | None - open to anyone | Password | +| **Transparency** | Measured - share what's confirmed | High - share everything you can | +| **Communication** | Reviewed status reports | Real-time metrics | +| **Search engines** | Indexed and discoverable | Hidden from crawlers | ## What Is a Public Status Page? diff --git a/apps/web/src/content/pages/guides/top-five-atlassian-statuspage-alternatives.mdx b/apps/web/src/content/pages/guides/top-five-atlassian-statuspage-alternatives.mdx index 478877ec..9434009d 100644 --- a/apps/web/src/content/pages/guides/top-five-atlassian-statuspage-alternatives.mdx +++ b/apps/web/src/content/pages/guides/top-five-atlassian-statuspage-alternatives.mdx @@ -44,29 +44,17 @@ While Statuspage is a powerful tool, several factors might lead your team to see Here's a high-level look at how these five alternatives stack up on key features. -
Status Page, "", "", "", "", ""], - ["Private Status Page", "$30/m", "$349/m", "✅", "$50/m", "Add on $42/m"], - ["Team Members", "Unlimited", "50 for $349/", "$42/seat", "25 for the $50/m plan", "$29/seat"], - ["Custom Style", "Theme Store", "✅ for $349", "✅", "✅", "$12 per page per month"], - [Monitoring, "", "", "", "", ""], - ["Monitoring", "✅", "❌", "✅", "✅", "✅"], - ["Monitoring As Code", "✅ Yaml based", "❌", "✅ Terraform", "❌", "✅ Terraform"], - ["OpenTelemetry", "✅", "❌", "✅", "❌", "✅"], - ["Private Location", "✅", "❌", "✅", "❌", "❌"], - ], - }} -/> +| Features | openstatus | status.io | Datadog Status Page | Instatus | Betterstack | +| --- | --- | --- | --- | --- | --- | +| **Status Page** | | | | | | +| Private Status Page | $30/m | $349/m | ✅ | $50/m | Add on $42/m | +| Team Members | Unlimited | 50 for $349/ | $42/seat | 25 for the $50/m plan | $29/seat | +| Custom Style | Theme Store | ✅ for $349 | ✅ | ✅ | $12 per page per month | +| **Monitoring** | | | | | | +| Monitoring | ✅ | ❌ | ✅ | ✅ | ✅ | +| Monitoring As Code | ✅ Yaml based | ❌ | ✅ Terraform | ❌ | ✅ Terraform | +| OpenTelemetry | ✅ | ❌ | ✅ | ❌ | ✅ | +| Private Location | ✅ | ❌ | ✅ | ❌ | ❌ | diff --git a/apps/web/src/content/pages/unrelated/pricing.mdx b/apps/web/src/content/pages/unrelated/pricing.mdx index 44c76600..ae9fb639 100644 --- a/apps/web/src/content/pages/unrelated/pricing.mdx +++ b/apps/web/src/content/pages/unrelated/pricing.mdx @@ -6,50 +6,40 @@ description: "All plans. Start free today, upgrade later." category: "company" --- -
$0/month|Hobby, - <>$30/month|Starter, - <>$100/month|Pro, - ], - rows: [ - [Monitors, "", "", ""], - ["Check Interval", "10m", "1m", "30s"], - ["Number of monitors", "1", "20", "50"], - ["Multi-region monitoring", "+", "+", "+"], - ["Total regions", "6", "28", "28"], - ["Regions per monitor", "6", "6", "28"], - ["Data retention", "14 days", "3 months", "12 months"], - ["Response logs", "", "+", "+"], - [Private locations, "", "", "+"], - ["OTel Exporter", "", "", "+"], - ["Number of on-demand checks", "30/mo.", "100/mo.", "300/mo."], - [Status Pages, "", "", ""], - ["Number of status pages", "1", "1 +$20/mo./each", "5 +$20/mo./each"], - ["Number of components", "3", "20", "50"], - ["Maintenance status", "+", "+", "+"], - ["Toggle numbers visibility", "+", "+", "+"], - ["Subscribers", "", "+", "+"], - ["Custom domain", "", "+", "+"], - ["White Label", "", "$300/mo.", "$300/mo."], - [Audience, "", "", ""], - ["Password Protection", "", "+", "+"], - ["Email Authentification", "", "$100/mo.", "$100/mo."], - [Alerts, "", "", ""], - ["Slack, Discord, Email, Webhook, ntfy.sh", "+", "+", "+"], - ["WhatsApp", "", "+", "+"], - ["SMS", "", "+", "+"], - ["PagerDuty", "", "+", "+"], - ["OpsGenie", "", "+", "+"], - ["Grafana OnCall", "", "+", "+"], - ["Number of notification channels", "1", "10", "20"], - [Collaboration, "", "", ""], - ["Team members", "1", "Unlimited", "Unlimited"], - ], - }} -/> +| Features comparison | $0/month - Hobby |$30/month - Starter | $100/month - Pro | +| --- | --- | --- | --- | +| [Monitors](/uptime-monitoring) | | | | +| Check Interval | 10m | 1m | 30s | +| Number of monitors | 1 | 20 | 50 | +| Multi-region monitoring | + | + | + | +| Total regions | 6 | 28 | 28 | +| Regions per monitor | 6 | 6 | 28 | +| Data retention | 14 days | 3 months | 12 months | +| Response logs | | + | + | +| [Private locations](/private-locations) | | | + | +| OTel Exporter | | | + | +| Number of on-demand checks | 30/mo. | 100/mo. | 300/mo. | +| [Status Pages](/status-page) | | | | +| Number of status pages | 1 | 1 +$20/mo./each | 5 +$20/mo./each | +| Number of components | 3 | 20 | 50 | +| Maintenance status | + | + | + | +| Toggle numbers visibility | + | + | + | +| Subscribers | | + | + | +| Custom domain | | + | + | +| White Label | | $300/mo. | $300/mo. | +| **Audience** | | | | +| Password Protection | | + | + | +| Email Authentification | | $100/mo. | $100/mo. | +| **Alerts** | | | | +| Slack | Discord | Email | Webhook | +| WhatsApp | | + | + | +| SMS | | + | + | +| PagerDuty | | + | + | +| OpsGenie | | + | + | +| Grafana OnCall | | + | + | +| Number of notification channels | 1 | 10 | 20 | +| **Collaboration** | | | | +| Team members | 1 | Unlimited | Unlimited | We provide pricing support for **EUR**/**USD**/**INR** as currency. Contact us at [ping@openstatus.dev](mailto:ping@openstatus.dev) or [book a call](https://openstatus.dev/cal) if you have questions. diff --git a/apps/web/src/content/resolve.ts b/apps/web/src/content/resolve.ts new file mode 100644 index 00000000..c7991e53 --- /dev/null +++ b/apps/web/src/content/resolve.ts @@ -0,0 +1,94 @@ +import { generateListingForPath } from "./listing"; +import type { MDXData } from "./utils"; +import { + getBlogPosts, + getChangelogPosts, + getComparePages, + getGuides, + getHomePage, + getProductPages, + getToolsPages, + getUnrelatedPages, +} from "./utils"; + +/** + * Content resolution result - either MDX content or a generated listing + */ +export type ContentResult = + | { type: "mdx"; data: MDXData } + | { type: "listing"; data: string }; + +/** + * Resolves pathname to content using two-tier fallback: + * 1. Try to find MDX content (blog posts, pages, etc.) + * 2. Fallback to generating sitemap-style listings + */ +export function resolveContent(pathname: string): ContentResult | null { + // Normalize pathname: remove trailing slash, decode URI + const normalizedPath = decodeURIComponent(pathname).replace(/\/$/, ""); + + // TIER 1: Try to find MDX content first + const mdxContent = resolveMdxContent(normalizedPath); + if (mdxContent) { + return { type: "mdx", data: mdxContent }; + } + + // TIER 2: Fallback to listing generation + const listing = generateListingForPath(normalizedPath); + if (listing) { + return { type: "listing", data: listing }; + } + + // Not found + return null; +} + +/** + * Resolves pathname to MDX content + */ +function resolveMdxContent(pathname: string): MDXData | null { + const segments = pathname.split("/").filter(Boolean); + + // Root path → home.mdx (confirmed: file exists) + if (segments.length === 0) { + try { + return getHomePage(); + } catch { + // home.mdx doesn't exist, will fallback to listing + return null; + } + } + + // Prefixed paths (category/slug format) + const [category, slug] = segments; + + // Skip /blog/category/slug pattern (handled by listing generator) + if (slug && slug !== "category") { + switch (category) { + case "blog": + return getBlogPosts().find((p) => p.slug === slug) ?? null; + case "changelog": + return getChangelogPosts().find((p) => p.slug === slug) ?? null; + case "compare": + return getComparePages().find((p) => p.slug === slug) ?? null; + case "guides": + return getGuides().find((p) => p.slug === slug) ?? null; + case "play": + return getToolsPages().find((p) => p.slug === slug) ?? null; + default: + return null; + } + } + + // Single segment: try unrelated, then product + if (segments.length === 1) { + const singleSlug = segments[0]; + return ( + getUnrelatedPages().find((p) => p.slug === singleSlug) ?? + getProductPages().find((p) => p.slug === singleSlug) ?? + null + ); + } + + return null; +} diff --git a/apps/web/src/content/sub-nav.tsx b/apps/web/src/content/sub-nav.tsx index 8929481f..3bf9e700 100644 --- a/apps/web/src/content/sub-nav.tsx +++ b/apps/web/src/content/sub-nav.tsx @@ -4,7 +4,7 @@ import { cn } from "@/lib/utils"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { Fragment } from "react"; -import { CopyButton } from "./copy-button"; +import { CopyDropdownButton } from "./copy-button"; export function SubNav({ className, ...props }: React.ComponentProps<"div">) { const pathname = usePathname(); @@ -27,11 +27,7 @@ export function SubNav({ className, ...props }: React.ComponentProps<"div">) { ))} - + ); } diff --git a/packages/icons/src/index.tsx b/packages/icons/src/index.tsx index 91c1c3b6..5781b1af 100644 --- a/packages/icons/src/index.tsx +++ b/packages/icons/src/index.tsx @@ -10,3 +10,4 @@ export * from "./railway"; export * from "./koyeb"; export * from "./telegram"; export * from "./whatsapp"; +export * from "./markdown"; diff --git a/packages/icons/src/markdown.tsx b/packages/icons/src/markdown.tsx new file mode 100644 index 00000000..d4331c13 --- /dev/null +++ b/packages/icons/src/markdown.tsx @@ -0,0 +1,13 @@ +export function Markdown(props: React.ComponentProps<"svg">) { + return ( + + Markdown + + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14da2c11..ddbebfd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,7 +73,7 @@ importers: version: 0.15.15 '@openpanel/nextjs': specifier: 1.0.8 - version: 1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@openstatus/analytics': specifier: workspace:* version: link:../../packages/analytics @@ -214,7 +214,7 @@ importers: version: 1.2.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: 10.31.0 - version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) + version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) '@stripe/stripe-js': specifier: 2.1.6 version: 2.1.6 @@ -229,7 +229,7 @@ importers: version: 11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3) '@trpc/next': specifier: 11.4.4 - version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@trpc/react-query': specifier: 11.4.4 version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -262,13 +262,13 @@ importers: version: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nuqs: specifier: 2.8.5 - version: 2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) random-word-slugs: specifier: 0.1.7 version: 0.1.7 @@ -629,7 +629,7 @@ importers: version: 0.15.15 '@openpanel/nextjs': specifier: 1.0.8 - version: 1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@openstatus/analytics': specifier: workspace:* version: link:../../packages/analytics @@ -722,7 +722,7 @@ importers: version: 1.2.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: 10.31.0 - version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) + version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) '@stripe/stripe-js': specifier: 2.1.6 version: 2.1.6 @@ -737,7 +737,7 @@ importers: version: 11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3) '@trpc/next': specifier: 11.4.4 - version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@trpc/react-query': specifier: 11.4.4 version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -770,16 +770,16 @@ importers: version: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) next-plausible: specifier: 3.12.5 - version: 3.12.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 3.12.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nuqs: specifier: 2.8.5 - version: 2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -876,7 +876,7 @@ importers: version: 0.15.15 '@openpanel/nextjs': specifier: 1.0.8 - version: 1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@openstatus/analytics': specifier: workspace:* version: link:../../packages/analytics @@ -954,7 +954,7 @@ importers: version: 1.2.3(@types/react@19.2.2)(react@19.2.3) '@sentry/nextjs': specifier: 10.31.0 - version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) + version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) '@stripe/stripe-js': specifier: 2.1.6 version: 2.1.6 @@ -981,7 +981,7 @@ importers: version: 11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3) '@trpc/next': specifier: 11.4.4 - version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@trpc/react-query': specifier: 11.4.4 version: 11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) @@ -1032,19 +1032,19 @@ importers: version: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) next-mdx-remote: specifier: 6.0.0 version: 6.0.0(@types/react@19.2.2)(react@19.2.3) next-plausible: specifier: 3.12.5 - version: 3.12.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 3.12.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) nuqs: specifier: 2.8.5 - version: 2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) random-word-slugs: specifier: 0.1.7 version: 0.1.7 @@ -1072,6 +1072,9 @@ importers: recharts: specifier: 2.15.0 version: 2.15.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + remark-gfm: + specifier: 4.0.1 + version: 4.0.1 resend: specifier: 6.6.0 version: 6.6.0(@react-email/render@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) @@ -1228,7 +1231,7 @@ importers: version: 2.6.2 drizzle-orm: specifier: 0.44.4 - version: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.8) + version: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.9) effect: specifier: 3.19.12 version: 3.19.12 @@ -1247,7 +1250,7 @@ importers: version: link:../../packages/tsconfig '@types/bun': specifier: latest - version: 1.3.8 + version: 1.3.9 typescript: specifier: 5.9.3 version: 5.9.3 @@ -1409,10 +1412,10 @@ importers: version: 3.0.3 drizzle-orm: specifier: 0.44.4 - version: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.8) + version: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.9) drizzle-zod: specifier: 0.8.3 - version: 0.8.3(drizzle-orm@0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.8))(zod@4.1.13) + version: 0.8.3(drizzle-orm@0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.9))(zod@4.1.13) zod: specifier: 4.1.13 version: 4.1.13 @@ -1431,7 +1434,7 @@ importers: version: 0.31.4 next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -6985,8 +6988,8 @@ packages: '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} - '@types/bun@1.3.8': - resolution: {integrity: sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA==} + '@types/bun@1.3.9': + resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} '@types/caseless@0.12.5': resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} @@ -7566,8 +7569,8 @@ packages: peerDependencies: '@types/react': ^19 - bun-types@1.3.8: - resolution: {integrity: sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q==} + bun-types@1.3.9: + resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -11542,9 +11545,6 @@ packages: unist-util-visit@4.1.2: resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} @@ -12236,7 +12236,7 @@ snapshots: smol-toml: 1.5.2 unified: 11.0.5 unist-util-remove-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 vfile: 6.0.3 transitivePeerDependencies: @@ -12256,7 +12256,7 @@ snapshots: remark-gfm: 4.0.1 remark-smartypants: 3.0.2 source-map: 0.7.6 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -12328,7 +12328,7 @@ snapshots: remark-directive: 3.0.1 ultrahtml: 1.6.0 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -13632,7 +13632,7 @@ snapshots: hastscript: 9.0.1 postcss: 8.4.38 postcss-nested: 6.2.0(postcss@8.4.38) - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 '@expressive-code/plugin-frames@0.41.3': @@ -14266,7 +14266,7 @@ snapshots: '@openpanel/web': 1.0.1 astro: 5.16.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(yaml@2.8.1) - '@openpanel/nextjs@1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@openpanel/nextjs@1.0.8(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@openpanel/web': 1.0.1 next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -16603,7 +16603,7 @@ snapshots: '@sentry/types': 8.9.2 '@sentry/utils': 8.9.2 - '@sentry/nextjs@10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0)': + '@sentry/nextjs@10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 @@ -17307,7 +17307,7 @@ snapshots: '@trpc/server': 11.4.4(typescript@5.9.3) typescript: 5.9.3 - '@trpc/next@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@trpc/next@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.2.3))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(@trpc/server@11.4.4(typescript@5.9.3))(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: '@trpc/client': 11.4.4(@trpc/server@11.4.4(typescript@5.9.3))(typescript@5.9.3) '@trpc/server': 11.4.4(typescript@5.9.3) @@ -17424,9 +17424,9 @@ snapshots: '@types/braces@3.0.5': {} - '@types/bun@1.3.8': + '@types/bun@1.3.9': dependencies: - bun-types: 1.3.8 + bun-types: 1.3.9 '@types/caseless@0.12.5': {} @@ -18000,7 +18000,7 @@ snapshots: tsconfck: 3.1.6(typescript@5.9.3) ultrahtml: 1.6.0 unifont: 0.6.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unstorage: 1.17.3 vfile: 6.0.3 vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.1)(yaml@2.8.1) @@ -18171,7 +18171,7 @@ snapshots: '@types/node': 24.0.8 '@types/react': 19.2.2 - bun-types@1.3.8: + bun-types@1.3.9: dependencies: '@types/node': 24.0.8 @@ -18699,16 +18699,16 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.8): + drizzle-orm@0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.9): optionalDependencies: '@libsql/client': 0.15.15 '@opentelemetry/api': 1.9.0 '@types/pg': 8.15.6 - bun-types: 1.3.8 + bun-types: 1.3.9 - drizzle-zod@0.8.3(drizzle-orm@0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.8))(zod@4.1.13): + drizzle-zod@0.8.3(drizzle-orm@0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.9))(zod@4.1.13): dependencies: - drizzle-orm: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.8) + drizzle-orm: 0.44.4(@libsql/client@0.15.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(bun-types@1.3.9) zod: 4.1.13 dset@3.1.4: {} @@ -19532,7 +19532,7 @@ snapshots: mdast-util-to-hast: 13.2.1 parse5: 7.3.0 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 web-namespaces: 2.0.1 zwitch: 2.0.4 @@ -19552,7 +19552,7 @@ snapshots: nth-check: 2.1.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 zwitch: 2.0.4 hast-util-to-estree@3.1.3: @@ -19625,7 +19625,7 @@ snapshots: rehype-minify-whitespace: 6.0.2 trim-trailing-lines: 2.1.0 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 hast-util-to-parse5@8.0.0: dependencies: @@ -20243,7 +20243,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 mdast-util-directive@3.1.0: dependencies: @@ -20403,7 +20403,7 @@ snapshots: micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 mdast-util-to-markdown@2.1.2: @@ -20415,7 +20415,7 @@ snapshots: mdast-util-to-string: 4.0.0 micromark-util-classify-character: 2.0.1 micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 zwitch: 2.0.4 mdast-util-to-string@4.0.0: @@ -20848,7 +20848,7 @@ snapshots: netmask@2.0.2: {} - next-auth@5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + next-auth@5.0.0-beta.29(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@auth/core': 0.40.0 next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -20868,7 +20868,7 @@ snapshots: - '@types/react' - supports-color - next-plausible@3.12.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-plausible@3.12.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: next: 16.0.10(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -20960,7 +20960,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + nuqs@2.8.5(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.3 @@ -21779,7 +21779,7 @@ snapshots: hast-util-heading-rank: 3.0.0 hast-util-is-element: 3.0.0 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 rehype-expressive-code@0.41.3: dependencies: @@ -21903,7 +21903,7 @@ snapshots: retext: 9.0.0 retext-smartypants: 6.2.0 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 remark-stringify@11.0.0: dependencies: @@ -21974,7 +21974,7 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 nlcst-to-string: 4.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 retext-stringify@4.0.0: dependencies: @@ -22448,7 +22448,7 @@ snapshots: '@astrojs/starlight': 0.37.1(astro@5.16.6(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.1)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(yaml@2.8.1)) mdast-util-mdx-jsx: 3.2.0 rehype-raw: 7.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 transitivePeerDependencies: - supports-color @@ -22467,7 +22467,7 @@ snapshots: mdast-util-to-string: 4.0.0 picomatch: 4.0.3 terminal-link: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 transitivePeerDependencies: - supports-color @@ -23077,7 +23077,7 @@ snapshots: unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-remove@4.0.0: dependencies: @@ -23113,12 +23113,6 @@ snapshots: unist-util-is: 5.2.1 unist-util-visit-parents: 5.1.3 - unist-util-visit@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - unist-util-visit@5.1.0: dependencies: '@types/unist': 3.0.3