Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: landing pages (#1235)

* feat: landing pages

* wip:

* feat: add more sections

* wip:

* feat: ci/cd page

* chore: gh action container

* chore: typo

* refactor: remove hero image and reorder blocks

authored by

Maximilian Kaske and committed by
GitHub
12915441 f27643f7

+1164 -435
+14
apps/web/next.config.js
··· 45 45 async headers() { 46 46 return [{ source: "/(.*)", headers: securityHeaders }]; 47 47 }, 48 + async redirects() { 49 + return [ 50 + { 51 + source: "/features/monitoring", 52 + destination: "/uptime-monitoring", 53 + permanent: true, 54 + }, 55 + { 56 + source: "/features/status-page", 57 + destination: "/status-page", 58 + permanent: true, 59 + }, 60 + ]; 61 + }, 48 62 async rewrites() { 49 63 return { 50 64 beforeFiles: [
+93
apps/web/src/app/(content)/(landing)/[slug]/page.tsx
··· 1 + import { 2 + defaultMetadata, 3 + ogMetadata, 4 + twitterMetadata, 5 + } from "@/app/shared-metadata"; 6 + import { landingsConfig } from "@/config/landings"; 7 + import { Button } from "@openstatus/ui"; 8 + import type { Metadata } from "next"; 9 + import Image from "next/image"; 10 + import Link from "next/link"; 11 + import { notFound } from "next/navigation"; 12 + 13 + export async function generateStaticParams() { 14 + return Object.keys(landingsConfig).map((slug) => ({ slug })); 15 + } 16 + export async function generateMetadata(props: { 17 + params: Promise<{ slug: string }>; 18 + }): Promise<Metadata | undefined> { 19 + const { slug } = await props.params; 20 + const landing = landingsConfig[slug as keyof typeof landingsConfig]; 21 + 22 + if (!landing) return; 23 + 24 + const { title, description } = landing; 25 + 26 + const encodedTitle = encodeURIComponent(title); 27 + const encodedDescription = encodeURIComponent(description); 28 + 29 + return { 30 + ...defaultMetadata, 31 + title, 32 + description, 33 + openGraph: { 34 + ...ogMetadata, 35 + title, 36 + description, 37 + images: [ 38 + `/api/og?title=${encodedTitle}&description=${encodedDescription}`, 39 + ], 40 + }, 41 + twitter: { 42 + ...twitterMetadata, 43 + title, 44 + description, 45 + images: [ 46 + `/api/og?title=${encodedTitle}&description=${encodedDescription}`, 47 + ], 48 + }, 49 + }; 50 + } 51 + 52 + export default async function Page({ 53 + params, 54 + }: { 55 + params: Promise<{ slug: string }>; 56 + }) { 57 + const { slug } = await params; 58 + const landing = landingsConfig[slug as keyof typeof landingsConfig]; 59 + 60 + if (!landing) { 61 + notFound(); 62 + } 63 + 64 + return ( 65 + <div className="grid gap-12"> 66 + <Hero title={landing.title} description={landing.description} /> 67 + {landing.blocks.map((block) => block)} 68 + </div> 69 + ); 70 + } 71 + 72 + function Hero({ title, description }: { title: string; description: string }) { 73 + return ( 74 + <div className="mx-auto my-12 sm:my-16 flex max-w-xl flex-col items-center gap-4 md:gap-6"> 75 + <div className="flex flex-col text-center gap-4 md:gap-6"> 76 + <h1 className="font-cal text-5xl leading-tight md:text-6xl">{title}</h1> 77 + <h2 className="mx-auto max-w-md text-xl text-muted-foreground md:max-w-xl md:text-2xl"> 78 + {description} 79 + </h2> 80 + </div> 81 + <div className="flex gap-2"> 82 + <Button className="rounded-full" asChild> 83 + <Link href="/app/login">Get Started</Link> 84 + </Button> 85 + <Button variant="outline" className="rounded-full" asChild> 86 + <a href="/cal" rel="noreferrer" target="_blank"> 87 + Book a demo 88 + </a> 89 + </Button> 90 + </div> 91 + </div> 92 + ); 93 + }
+3 -32
apps/web/src/app/(content)/compare/[slug]/page.tsx
··· 4 4 twitterMetadata, 5 5 } from "@/app/shared-metadata"; 6 6 import { Shell } from "@/components/dashboard/shell"; 7 + import { BookingBanner } from "@/components/marketing/banner/booking-banner"; 7 8 import { FAQs } from "@/components/marketing/faqs"; 8 9 import { PricingSlider } from "@/components/marketing/pricing/pricing-slider"; 9 10 import { alternativesConfig as config } from "@/config/alternatives"; ··· 15 16 BreadcrumbPage, 16 17 BreadcrumbSeparator, 17 18 } from "@openstatus/ui/src/components/breadcrumb"; 18 - import { Button } from "@openstatus/ui/src/components/button"; 19 19 import type { Metadata } from "next"; 20 20 import Image from "next/image"; 21 21 import Link from "next/link"; ··· 38 38 const description = `Looking for a ${name} alternative? OpenStatus is an open-source alternative to ${name}. Try it out for free.`; 39 39 40 40 const encodedTitle = encodeURIComponent(title); 41 - // TODO: check if there is a better wording 41 + 42 42 const encodedDescription = encodeURIComponent( 43 43 "Compare both and pick what fits best to you.", 44 44 ); ··· 70 70 }, 71 71 }; 72 72 } 73 - // add to sitemap 74 73 75 74 export default async function Page({ 76 75 params, ··· 120 119 </div> 121 120 <ComparisonTable slug={slug} /> 122 121 </Shell> 123 - <Shell className="flex flex-col gap-6 bg-muted md:flex-row md:items-center md:justify-between"> 124 - <div> 125 - <p className="font-semibold text-2xl"> 126 - Don't talk to Sales. Talk to Founders. 127 - </p> 128 - <p className="text-muted-foreground text-sm"> 129 - We are here to help you with any questions or concerns you may 130 - have. 131 - </p> 132 - </div> 133 - <div className="flex gap-2"> 134 - <Button className="rounded-full" variant="outline" asChild> 135 - <Link href="/app/login" className="text-nowrap"> 136 - Start for free 137 - </Link> 138 - </Button> 139 - <Button className="rounded-full" asChild> 140 - <a 141 - target="_blank" 142 - rel="noreferrer" 143 - href="https://cal.com/team/openstatus/30min" 144 - className="text-nowrap" 145 - > 146 - Talk to us 147 - </a> 148 - </Button> 149 - </div> 150 - </Shell> 151 - {/* FIXME: responsive design */} 122 + <BookingBanner /> 152 123 <Shell className="w-full max-w-4xl"> 153 124 <PricingSlider /> 154 125 </Shell>
apps/web/src/app/(content)/features/_components/assertions-timing-form-example.tsx apps/web/src/components/marketing/feature/assertions-timing-form-example.tsx
-22
apps/web/src/app/(content)/features/_components/banner.tsx
··· 1 - import { Shell } from "@/components/dashboard/shell"; 2 - import { Button } from "@openstatus/ui/src/components/button"; 3 - import Link from "next/link"; 4 - 5 - export function Banner() { 6 - return ( 7 - <Shell className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between"> 8 - <p className="max-w-xl font-medium text-lg"> 9 - Learn how your services are performing over time, and notify your users 10 - of any issues. 11 - </p> 12 - <div className="flex items-center gap-2 self-end"> 13 - <Button variant="outline" className="min-w-max rounded-full" asChild> 14 - <Link href="/cal">Book a call</Link> 15 - </Button> 16 - <Button className="min-w-max rounded-full" asChild> 17 - <Link href="/app/login">Get started</Link> 18 - </Button> 19 - </div> 20 - </Shell> 21 - ); 22 - }
-31
apps/web/src/app/(content)/features/_components/hero.tsx
··· 1 - import { cn } from "@/lib/utils"; 2 - import type * as React from "react"; 3 - 4 - interface HeroProps extends React.ComponentPropsWithoutRef<"div"> { 5 - title: string; 6 - subTitle: string; 7 - } 8 - 9 - export function Hero({ title, subTitle, className, ...props }: HeroProps) { 10 - return ( 11 - <div 12 - className={cn( 13 - "mx-auto my-16 flex max-w-xl flex-col items-center gap-4", 14 - className, 15 - )} 16 - {...props} 17 - > 18 - <h1 19 - className={cn( 20 - "text-center font-cal text-5xl leading-tight", 21 - "bg-gradient-to-tl from-0% from-[hsl(var(--muted))] to-30% to-[hsl(var(--foreground))] bg-clip-text text-transparent", 22 - )} 23 - > 24 - {title} 25 - </h1> 26 - <h2 className="mx-auto max-w-md text-center text-lg text-muted-foreground md:max-w-xl md:text-xl"> 27 - {subTitle} 28 - </h2> 29 - </div> 30 - ); 31 - }
+5 -3
apps/web/src/app/(content)/features/_components/interactive-feature.tsx apps/web/src/components/marketing/feature/interactive-feature.tsx
··· 22 22 }, 23 23 ); 24 24 25 - interface InteractiveFeatureProps 25 + export interface InteractiveFeatureProps 26 26 extends React.ComponentPropsWithoutRef<"div"> { 27 27 component: React.ReactNode; 28 28 icon: ValidIcon; ··· 50 50 position, 51 51 col, 52 52 withGradient = false, 53 + className, 53 54 }: InteractiveFeatureProps) { 54 55 const Component = component; 55 56 const isSingleCol = ["top", "bottom"].includes(position); ··· 86 87 : "left", 87 88 }), 88 89 "relative", 90 + className, 89 91 )} 90 92 > 91 93 {Component} ··· 131 133 function FeatureSubheader({ icon, text }: { icon: ValidIcon; text: string }) { 132 134 const Icon = Icons[icon]; 133 135 return ( 134 - <h3 className="flex items-center gap-2 text-muted-foreground"> 136 + <h3 className="flex items-center gap-2 text-muted-foreground text-sm md:text-base"> 135 137 <Icon className="h-4 w-4 text-foreground" /> 136 138 {text} 137 139 </h3> ··· 144 146 145 147 function FeatureTitle({ strong, regular }: FeatureTitleProps) { 146 148 return ( 147 - <p className="text-muted-foreground text-xl"> 149 + <p className="text-muted-foreground text-lg md:text-xl"> 148 150 <strong className="font-medium text-foreground">{strong}</strong>{" "} 149 151 {regular} 150 152 </p>
+1 -1
apps/web/src/app/(content)/features/_components/tracker-example.tsx apps/web/src/components/marketing/feature/tracker-example.tsx
··· 3 3 import { Tracker } from "@/components/tracker/tracker"; 4 4 import { Checkbox } from "@openstatus/ui"; 5 5 import { useState } from "react"; 6 - import { mockTrackerData } from "../mock"; 6 + import { mockTrackerData } from "./mock"; 7 7 8 8 export function TrackerWithVisibilityToggle() { 9 9 const [visible, setVisible] = useState(true);
apps/web/src/app/(content)/features/mock.ts apps/web/src/components/marketing/feature/mock.ts
-154
apps/web/src/app/(content)/features/monitoring/page.tsx
··· 1 - import { 2 - defaultMetadata, 3 - ogMetadata, 4 - twitterMetadata, 5 - } from "@/app/shared-metadata"; 6 - import { Mdx } from "@/components/content/mdx"; 7 - import { Chart } from "@/components/monitor-charts/chart"; 8 - import { RegionsPreset } from "@/components/monitor-dashboard/region-preset"; 9 - import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 10 - import { marketingProductPagesConfig } from "@/config/pages"; 11 - import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 12 - import { Button } from "@openstatus/ui/src/components/button"; 13 - import { Skeleton } from "@openstatus/ui/src/components/skeleton"; 14 - import { allUnrelateds } from "content-collections"; 15 - import type { Metadata } from "next"; 16 - import Link from "next/link"; 17 - import { Suspense } from "react"; 18 - import { AssertionsTimingFormExample } from "../_components/assertions-timing-form-example"; 19 - import { Banner } from "../_components/banner"; 20 - import { Hero } from "../_components/hero"; 21 - import { InteractiveFeature } from "../_components/interactive-feature"; 22 - import { mockChartData, mockResponseData } from "../mock"; 23 - 24 - const { description, subtitle } = marketingProductPagesConfig[0]; 25 - const code = allUnrelateds.find( 26 - (unrelated) => unrelated.slug === "ci-cd-features-block", 27 - ); 28 - 29 - export const metadata: Metadata = { 30 - ...defaultMetadata, 31 - title: "API & Website Monitoring", 32 - description: 33 - "Get insights of the latency of your API and website from all over the world.", 34 - twitter: { 35 - ...twitterMetadata, 36 - title: "API & Website Monitoring", 37 - description: 38 - "Get insights of the latency of your API and website from all over the world.", 39 - }, 40 - openGraph: { 41 - ...ogMetadata, 42 - title: "API & Website Monitoring", 43 - description: 44 - "Get insights of the latency of your API and website from all over the world.", 45 - }, 46 - }; 47 - 48 - export default function FeaturePage() { 49 - return ( 50 - <div className="grid w-full gap-12"> 51 - <Hero title={description} subTitle={subtitle} /> 52 - <InteractiveFeature 53 - icon="activity" 54 - iconText="Website & API monitoring" 55 - title="Global Monitoring." 56 - subTitle="Get insights of the latency worldwide." 57 - component={ 58 - <Suspense fallback={<Skeleton />}> 59 - <div className="m-auto"> 60 - <RegionsPreset 61 - regions={flyRegions as unknown as Region[]} 62 - selectedRegions={flyRegions as unknown as Region[]} 63 - /> 64 - </div> 65 - </Suspense> 66 - } 67 - col={1} 68 - position={"left"} 69 - /> 70 - <InteractiveFeature 71 - icon="book-open-check" 72 - iconText="Timing & Assertions" 73 - title="Validate the response." 74 - subTitle="Check the return value, status code, header or maximum response time." 75 - component={ 76 - <Suspense fallback={<Skeleton />}> 77 - <AssertionsTimingFormExample />{" "} 78 - </Suspense> 79 - } 80 - col={2} 81 - position={"left"} 82 - withGradient 83 - /> 84 - <InteractiveFeature 85 - icon="timer" 86 - iconText="Request Metrics Insights" 87 - title="Optimize Web Performance." 88 - subTitle="Analyze DNS, TCP, TLS, and TTFB for every request and inspect Response Headers as needed." 89 - component={ 90 - <Suspense fallback={<Skeleton />}> 91 - <ResponseDetailTabs 92 - {...mockResponseData} 93 - defaultOpen="timing" 94 - hideInfo={false} 95 - /> 96 - </Suspense> 97 - } 98 - col={2} 99 - position={"left"} 100 - withGradient 101 - /> 102 - <InteractiveFeature 103 - icon="line-chart" 104 - iconText="Charts" 105 - title="Opinionated Dashboard." 106 - subTitle="Keep an overview about Uptime, P50, P75, P90, P95, P99 of your monitors." 107 - action={ 108 - <div className="mt-2"> 109 - <Button variant="outline" className="rounded-full" asChild> 110 - <Link href="/public/monitors/1">Public Dashboard</Link> 111 - </Button> 112 - </div> 113 - } 114 - component={ 115 - <Suspense fallback={"loading..."}> 116 - <Chart {...mockChartData} /> 117 - </Suspense> 118 - } 119 - col={2} 120 - position={"top"} 121 - withGradient 122 - /> 123 - <InteractiveFeature 124 - icon="bot" 125 - iconText="API Monitoring" 126 - title="Synthetic Monitoring." 127 - subTitle="Run your check in your CI/CD pipeline or on demand." 128 - component={ 129 - code ? ( 130 - <Mdx 131 - code={code.mdx} 132 - className="max-w-none prose-pre:overflow-hidden" 133 - /> 134 - ) : ( 135 - <p>Code not found</p> 136 - ) 137 - } 138 - action={ 139 - <div className="mt-2"> 140 - <Button variant="outline" className="rounded-full" asChild> 141 - <Link href="https://docs.openstatus.dev/cli/getting-started"> 142 - How-to 143 - </Link> 144 - </Button> 145 - </div> 146 - } 147 - col={2} 148 - position={"bottom"} 149 - withGradient 150 - /> 151 - <Banner /> 152 - </div> 153 - ); 154 - }
-143
apps/web/src/app/(content)/features/status-page/page.tsx
··· 1 - import { 2 - defaultMetadata, 3 - ogMetadata, 4 - twitterMetadata, 5 - } from "@/app/shared-metadata"; 6 - import { PasswordFormSuspense } from "@/app/status-page/[domain]/_components/password-form"; 7 - import { SubscribeButton } from "@/app/status-page/[domain]/_components/subscribe-button"; 8 - import { MaintenanceContainer } from "@/components/status-page/maintenance"; 9 - import { StatusCheck } from "@/components/status-page/status-check"; 10 - import { StatusReport } from "@/components/status-page/status-report"; 11 - import { marketingProductPagesConfig } from "@/config/pages"; 12 - import { Button, InputWithAddons } from "@openstatus/ui"; 13 - import type { Metadata } from "next"; 14 - import Link from "next/link"; 15 - import { Banner } from "../_components/banner"; 16 - import { Hero } from "../_components/hero"; 17 - import { InteractiveFeature } from "../_components/interactive-feature"; 18 - import { TrackerWithVisibilityToggle } from "../_components/tracker-example"; 19 - import { maintenanceData, statusReportData } from "../mock"; 20 - 21 - const { description, subtitle } = marketingProductPagesConfig[1]; 22 - 23 - export const metadata: Metadata = { 24 - ...defaultMetadata, 25 - title: "Status Page", 26 - description: 27 - "Easily report to your users with our public or private status page.", 28 - twitter: { 29 - ...twitterMetadata, 30 - title: "Status Page", 31 - description: 32 - "Easily report to your users with our public or private status page.", 33 - }, 34 - openGraph: { 35 - ...ogMetadata, 36 - title: "Status Page", 37 - description: 38 - "Easily report to your users with our public or private status page.", 39 - }, 40 - }; 41 - 42 - export default function FeaturePage() { 43 - return ( 44 - <div className="grid w-full gap-12"> 45 - <Hero title={description} subTitle={subtitle} /> 46 - <InteractiveFeature 47 - icon="globe" 48 - iconText="Customize" 49 - title="Custom Domain." 50 - subTitle="Bring your own domain, give the status page a personal touch." 51 - component={ 52 - <div className="m-auto"> 53 - <InputWithAddons leading="https://" placeholder="status.acme.com" /> 54 - </div> 55 - } 56 - col={1} 57 - position={"left"} 58 - /> 59 - <InteractiveFeature 60 - icon="panel-top" 61 - iconText="Simple by default" 62 - title="Status page." 63 - subTitle="Connect your monitors and inform your users about the uptime." 64 - component={<TrackerWithVisibilityToggle />} 65 - col={2} 66 - position={"left"} 67 - /> 68 - <InteractiveFeature 69 - icon="users" 70 - iconText="Reach your users" 71 - title="Subscriptions." 72 - subTitle="Let your users subscribe to your status page, to automatically receive updates about the status of your services." 73 - component={ 74 - <div className="m-auto"> 75 - <SubscribeButton plan="team" slug={""} isDemo /> 76 - </div> 77 - } 78 - col={1} 79 - position={"left"} 80 - /> 81 - <InteractiveFeature 82 - icon="search-check" 83 - iconText="Stay up to date" 84 - title="Status Updates." 85 - subTitle="Down't let your users in the dark and show what's wrong." 86 - component={ 87 - <div className="-translate-y-6 m-auto scale-[0.80]"> 88 - <StatusReport isDemo {...statusReportData} /> 89 - </div> 90 - } 91 - col={1} 92 - position={"bottom"} 93 - withGradient 94 - /> 95 - <InteractiveFeature 96 - icon="eye-off" 97 - iconText="Restrict access" 98 - title="Password Protection." 99 - subTitle="Hide your page to unexepected users." 100 - component={ 101 - <div className="m-auto max-w-lg"> 102 - <PasswordFormSuspense slug="" /> 103 - </div> 104 - } 105 - col={2} 106 - position={"left"} 107 - /> 108 - <InteractiveFeature 109 - icon="user" 110 - iconText="Keep it simple" 111 - title="Build trust." 112 - subTitle="Showcase your reliability to your users, and reduce the number of customer service tickets." 113 - component={<StatusCheck />} 114 - action={ 115 - <div className="mt-2"> 116 - <Button variant="outline" className="rounded-full" asChild> 117 - <Link href="https://status.openstatus.dev">Status Page</Link> 118 - </Button> 119 - </div> 120 - } 121 - col={2} 122 - position={"bottom"} 123 - /> 124 - <InteractiveFeature 125 - icon="hammer" 126 - iconText="Handle migrations" 127 - title="Maintenance." 128 - subTitle="Mute your monitors for a specific period and inform the users about upcoming maintenance." 129 - component={ 130 - <div className="m-auto scale-[0.80]"> 131 - <MaintenanceContainer 132 - className="rounded-lg border-status-monitoring/10 bg-status-monitoring/5" 133 - {...maintenanceData} 134 - /> 135 - </div> 136 - } 137 - col={2} 138 - position={"left"} 139 - /> 140 - <Banner /> 141 - </div> 142 - ); 143 - }
+7 -3
apps/web/src/app/sitemap.ts
··· 1 1 import { alternativesConfig } from "@/config/alternatives"; 2 + import { landingsConfig } from "@/config/landings"; 2 3 import { allChangelogs, allPosts } from "content-collections"; 3 4 import type { MetadataRoute } from "next"; 4 5 ··· 20 21 lastModified: new Date(), 21 22 })); 22 23 24 + const landings = Object.keys(landingsConfig).map((slug) => ({ 25 + url: `https://www.openstatus.dev/${slug}`, 26 + lastModified: new Date(), 27 + })); 28 + 23 29 const routes = [ 24 30 "/", 25 31 "/about", 26 32 "/app/login", 27 33 "/blog", 28 34 "/changelog", 29 - "/features/status-page", 30 - "/features/monitoring", 31 35 "/play", 32 36 "/play/checker", 33 37 "/play/curl", ··· 38 42 lastModified: new Date(), 39 43 })); 40 44 41 - return [...routes, ...blogs, ...changelogs, ...comparisons]; 45 + return [...routes, ...blogs, ...changelogs, ...comparisons, ...landings]; 42 46 }
+4
apps/web/src/components/icons.tsx
··· 12 12 Check, 13 13 Clock, 14 14 Cog, 15 + Command, 15 16 Copy, 16 17 CreditCard, 17 18 Eye, ··· 43 44 Play, 44 45 Plug, 45 46 Puzzle, 47 + Radar, 46 48 Ratio, 47 49 Search, 48 50 SearchCheck, ··· 133 135 "book-open-check": BookOpenCheck, 134 136 info: Info, 135 137 server: Server, 138 + command: Command, 139 + radar: Radar, 136 140 discord: ({ ...props }: LucideProps) => ( 137 141 <svg viewBox="0 0 640 512" {...props}> 138 142 <path
+25 -12
apps/web/src/components/layout/marketing-footer.tsx
··· 3 3 4 4 import { ThemeToggle } from "@/components/theme/theme-toggle"; 5 5 import { alternativesConfig } from "@/config/alternatives"; 6 + import { landingsConfig } from "@/config/landings"; 6 7 import { socialsConfig } from "@/config/socials"; 7 8 import { cn } from "@/lib/utils"; 8 9 import { Shell } from "../dashboard/shell"; ··· 18 19 return ( 19 20 <footer className={cn("w-full", className)}> 20 21 <Shell className="grid gap-6"> 21 - <div className="grid grid-cols-1 gap-6 sm:grid-cols-3 md:grid-cols-11"> 22 - <div className="col-span-2 flex flex-col gap-3 md:col-span-3"> 22 + <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-5 md:grid-rows-2"> 23 + <div className="col-span-1 flex flex-col gap-3 md:col-span-2 md:row-span-2"> 23 24 <div> 24 25 <BrandName /> 25 26 <p className="mt-2 max-w-md font-light text-muted-foreground text-sm"> ··· 33 34 </div> 34 35 <StatusWidgetContainer slug="status" /> 35 36 </div> 36 - <div className="col-span-2 flex flex-col gap-3 text-sm sm:col-span-1 md:col-span-2"> 37 - <p className="font-semibold text-foreground">Resources</p> 38 - <FooterLink href="/blog" label="Blog" /> 39 - <FooterLink href="/pricing" label="Pricing" /> 40 - <FooterLink href="https://docs.openstatus.dev" label="Docs" /> 41 - <FooterLink href="/oss-friends" label="OSS Friends" /> 42 - <FooterLink href="/status" label="External Providers Monitoring" /> 37 + <div className="col-span-1 flex flex-col gap-3 text-sm"> 38 + <p className="font-semibold text-foreground">Product</p> 39 + {Object.keys(landingsConfig).map((slug) => ( 40 + <FooterLink 41 + key={slug} 42 + href={`/${slug}`} 43 + label={ 44 + landingsConfig[slug as keyof typeof landingsConfig].title 45 + } 46 + /> 47 + ))} 43 48 </div> 44 - <div className="col-span-2 flex flex-col gap-3 text-sm sm:col-span-1 md:col-span-2"> 49 + <div className="col-span-1 flex flex-col gap-3 text-sm"> 45 50 <p className="font-semibold text-foreground">Compare</p> 46 51 {Object.keys(alternativesConfig).map((slug) => ( 47 52 <FooterLink ··· 54 59 /> 55 60 ))} 56 61 </div> 57 - <div className="col-span-2 flex flex-col gap-3 text-sm sm:col-span-1 md:col-span-2"> 62 + <div className="col-span-1 flex flex-col gap-3 text-sm"> 58 63 <p className="font-semibold text-foreground">Company</p> 59 64 <FooterLink href="/about" label="About" /> 60 65 <FooterLink href="/changelog" label="Changelog" /> 61 66 <FooterLink href="/legal/terms" label="Terms" /> 62 67 <FooterLink href="/legal/privacy" label="Privacy" /> 63 68 </div> 64 - <div className="col-span-2 flex flex-col gap-3 text-sm sm:col-span-1 md:col-span-2"> 69 + <div className="col-span-1 flex flex-col gap-3 text-sm"> 70 + <p className="font-semibold text-foreground">Resources</p> 71 + <FooterLink href="/blog" label="Blog" /> 72 + <FooterLink href="/pricing" label="Pricing" /> 73 + <FooterLink href="https://docs.openstatus.dev" label="Docs" /> 74 + <FooterLink href="/oss-friends" label="OSS Friends" /> 75 + <FooterLink href="/status" label="External Providers Monitoring" /> 76 + </div> 77 + <div className="col-span-1 flex flex-col gap-3 text-sm"> 65 78 <p className="font-semibold text-foreground">Tools</p> 66 79 <FooterLink href="/play" label="Playground" /> 67 80 <FooterLink href="/play/checker" label="Speed Checker" />
+31
apps/web/src/components/marketing/banner/booking-banner.tsx
··· 1 + import { Button } from "@openstatus/ui"; 2 + import Link from "next/link"; 3 + import { GenericBanner } from "./generic-banner"; 4 + 5 + export function BookingBanner() { 6 + return ( 7 + <GenericBanner 8 + title="Don't talk to Sales. Talk to Founders." 9 + description="We are here to help you with any questions or concerns you may have." 10 + actions={ 11 + <div className="flex gap-2"> 12 + <Button className="rounded-full" variant="outline" asChild> 13 + <Link href="/app/login" className="text-nowrap"> 14 + Start for free 15 + </Link> 16 + </Button> 17 + <Button className="rounded-full" asChild> 18 + <a 19 + target="_blank" 20 + rel="noreferrer" 21 + href="https://cal.com/team/openstatus/30min" 22 + className="text-nowrap" 23 + > 24 + Talk to us 25 + </a> 26 + </Button> 27 + </div> 28 + } 29 + /> 30 + ); 31 + }
+31
apps/web/src/components/marketing/banner/enterprise-banner.tsx
··· 1 + import { Button } from "@openstatus/ui"; 2 + import Link from "next/link"; 3 + import { GenericBanner } from "./generic-banner"; 4 + 5 + export function EnterpriseBanner() { 6 + return ( 7 + <GenericBanner 8 + title="Looking for an enterprise solution?" 9 + description="We offer custom solutions for large organizations." 10 + actions={ 11 + <div className="flex gap-2"> 12 + <Button className="rounded-full" variant="outline" asChild> 13 + <Link href="/app/login" className="text-nowrap"> 14 + Start for free 15 + </Link> 16 + </Button> 17 + <Button className="rounded-full" asChild> 18 + <a 19 + target="_blank" 20 + rel="noreferrer" 21 + href="https://cal.com/team/openstatus/30min" 22 + className="text-nowrap" 23 + > 24 + Book a call 25 + </a> 26 + </Button> 27 + </div> 28 + } 29 + /> 30 + ); 31 + }
+33
apps/web/src/components/marketing/banner/generic-banner.tsx
··· 1 + import { Shell } from "@/components/dashboard/shell"; 2 + import { cn } from "@/lib/utils"; 3 + 4 + interface GenericBannerProps { 5 + title: string; 6 + description: string; 7 + className?: string; 8 + actions: React.ReactNode; 9 + } 10 + 11 + export function GenericBanner({ 12 + title, 13 + description, 14 + className, 15 + actions, 16 + }: GenericBannerProps) { 17 + return ( 18 + <Shell 19 + className={cn( 20 + "flex flex-col gap-3 bg-accent/50 md:flex-row md:items-center md:justify-between", 21 + className, 22 + )} 23 + > 24 + <div> 25 + <p className="font-semibold text-lg md:text-xl">{title}</p> 26 + <p className="text-muted-foreground text-sm md:text-base"> 27 + {description} 28 + </p> 29 + </div> 30 + {actions} 31 + </Shell> 32 + ); 33 + }
+35
apps/web/src/components/marketing/banner/speed-banner.tsx
··· 1 + import { Button } from "@openstatus/ui"; 2 + import Link from "next/link"; 3 + import { SpeedCheckerButton } from "../speed-checker-button"; 4 + import { GenericBanner } from "./generic-banner"; 5 + 6 + const config = { 7 + exploratory: { 8 + title: "Type. Submit. Discover.", 9 + description: "See how fast your API really is - in real time.", 10 + }, 11 + dev: { 12 + title: "Call. Measuer. Improve.", 13 + description: "Benchmark your API speed with OpenStatus.", 14 + }, 15 + }; 16 + 17 + export function SpeedBanner() { 18 + const { title, description } = config.exploratory; 19 + return ( 20 + <GenericBanner 21 + title={title} 22 + description={description} 23 + actions={ 24 + <div className="flex gap-2"> 25 + <Button className="rounded-full" variant="outline" asChild> 26 + <Link href="/app/login" className="text-nowrap"> 27 + Start for free 28 + </Link> 29 + </Button> 30 + <SpeedCheckerButton /> 31 + </div> 32 + } 33 + /> 34 + ); 35 + }
+556
apps/web/src/components/marketing/feature/index.tsx
··· 1 + import { PasswordFormSuspense } from "@/app/status-page/[domain]/_components/password-form"; 2 + import { SubscribeButton } from "@/app/status-page/[domain]/_components/subscribe-button"; 3 + import { Mdx } from "@/components/content/mdx"; 4 + import { Chart } from "@/components/monitor-charts/chart"; 5 + import { RegionsPreset } from "@/components/monitor-dashboard/region-preset"; 6 + import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 7 + import { MaintenanceContainer } from "@/components/status-page/maintenance"; 8 + import { StatusCheck } from "@/components/status-page/status-check"; 9 + import { StatusReport } from "@/components/status-page/status-report"; 10 + import { Tracker } from "@/components/tracker/tracker"; 11 + import type { Region } from "@openstatus/db/src/schema/constants"; 12 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 13 + import { Button, InputWithAddons } from "@openstatus/ui"; 14 + import { Skeleton } from "@openstatus/ui/src/components/skeleton"; 15 + import { allUnrelateds } from "content-collections"; 16 + import Link from "next/link"; 17 + import { Suspense } from "react"; 18 + import { AssertionsTimingFormExample } from "./assertions-timing-form-example"; 19 + import { 20 + InteractiveFeature, 21 + type InteractiveFeatureProps, 22 + } from "./interactive-feature"; 23 + import { 24 + maintenanceData, 25 + mockChartData, 26 + mockResponseData, 27 + mockTrackerData, 28 + statusReportData, 29 + } from "./mock"; 30 + import { NotificationsFormExample } from "./notifications-form-example"; 31 + import { RaycastExample } from "./raycast-example"; 32 + import { TrackerWithVisibilityToggle } from "./tracker-example"; 33 + 34 + export { BookingBanner } from "../banner/booking-banner"; 35 + export { SpeedBanner } from "../banner/speed-banner"; 36 + 37 + export function FeatureTimingAssertions( 38 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 39 + ) { 40 + return ( 41 + <InteractiveFeature 42 + icon="book-open-check" 43 + iconText="Timing & Assertions" 44 + title="Validate the response." 45 + subTitle="Check the return value, status code, header or maximum response time." 46 + component={ 47 + <div className="origin-top scale-[0.80]"> 48 + <Suspense fallback={<Skeleton />}> 49 + <AssertionsTimingFormExample /> 50 + </Suspense> 51 + </div> 52 + } 53 + col={2} 54 + position={props.position || "left"} 55 + withGradient 56 + /> 57 + ); 58 + } 59 + 60 + export function FeatureNotifications( 61 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 62 + ) { 63 + return ( 64 + <InteractiveFeature 65 + icon="bell" 66 + iconText="Notifications" 67 + title="Integrate your channels." 68 + subTitle="Get notified when your services are down. Slack, Discord, Email, and more." 69 + component={ 70 + <div className="scale-[0.80] my-auto"> 71 + <NotificationsFormExample /> 72 + </div> 73 + } 74 + col={2} 75 + position={props.position || "right"} 76 + /> 77 + ); 78 + } 79 + 80 + export function FeatureStatusPageTracker( 81 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 82 + ) { 83 + return ( 84 + <InteractiveFeature 85 + icon="panel-top" 86 + iconText="Simple by default" 87 + title="Status page." 88 + subTitle="Connect your monitors and inform your users about the uptime." 89 + component={ 90 + <div className="my-auto"> 91 + <Tracker 92 + data={mockTrackerData} 93 + name="OpenStatus" 94 + description="Website Health" 95 + showValues 96 + /> 97 + </div> 98 + } 99 + col={2} 100 + position={props.position || "left"} 101 + /> 102 + ); 103 + } 104 + 105 + export function FeatureCharts( 106 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 107 + ) { 108 + return ( 109 + <InteractiveFeature 110 + icon="line-chart" 111 + iconText="Charts" 112 + title="Opinionated Dashboard." 113 + subTitle="Keep an overview about Uptime, P50, P75, P90, P95, P99 of your monitors. Share it with your team or make it public." 114 + action={ 115 + <div className="mt-2"> 116 + <Button variant="outline" className="rounded-full" asChild> 117 + <Link href="/public/monitors/1">Public Dashboard</Link> 118 + </Button> 119 + </div> 120 + } 121 + component={ 122 + <div className="origin-top my-auto scale-[0.80]"> 123 + <Suspense fallback={<Skeleton />}> 124 + <Chart {...mockChartData} /> 125 + </Suspense> 126 + </div> 127 + } 128 + col={2} 129 + position={props.position || "top"} 130 + withGradient 131 + /> 132 + ); 133 + } 134 + 135 + export function FeatureCustomDomain( 136 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 137 + ) { 138 + return ( 139 + <InteractiveFeature 140 + icon="globe" 141 + iconText="Customize" 142 + title="Custom Domain." 143 + subTitle="Bring your own domain, give the status page a personal touch." 144 + component={ 145 + <div className="m-auto"> 146 + <InputWithAddons leading="https://" placeholder="status.acme.com" /> 147 + </div> 148 + } 149 + col={1} 150 + position={props.position || "left"} 151 + /> 152 + ); 153 + } 154 + 155 + export function FeatureStatusPageTrackerToggle( 156 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 157 + ) { 158 + return ( 159 + <InteractiveFeature 160 + icon="panel-top" 161 + iconText="Simple by default" 162 + title="Status page." 163 + subTitle="Connect your monitors and inform your users about the uptime." 164 + component={<TrackerWithVisibilityToggle />} 165 + col={2} 166 + position={props.position || "left"} 167 + /> 168 + ); 169 + } 170 + 171 + export function FeatureSubscriptions( 172 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 173 + ) { 174 + return ( 175 + <InteractiveFeature 176 + icon="users" 177 + iconText="Reach your users" 178 + title="Subscriptions." 179 + subTitle="Let your users subscribe to your status page, to automatically receive updates about the status of your services." 180 + component={ 181 + <div className="m-auto"> 182 + <SubscribeButton plan="team" slug={""} isDemo /> 183 + </div> 184 + } 185 + col={1} 186 + position={props.position || "left"} 187 + /> 188 + ); 189 + } 190 + 191 + export function FeatureStatusUpdates( 192 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 193 + ) { 194 + return ( 195 + <InteractiveFeature 196 + icon="search-check" 197 + iconText="Stay up to date" 198 + title="Status Updates." 199 + subTitle="Down't let your users in the dark and show what's wrong." 200 + component={ 201 + <div className="my-auto origin-top scale-[0.80]"> 202 + <StatusReport isDemo {...statusReportData} /> 203 + </div> 204 + } 205 + col={1} 206 + position={props.position || "bottom"} 207 + withGradient 208 + /> 209 + ); 210 + } 211 + 212 + export function FeaturePasswordProtection( 213 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 214 + ) { 215 + return ( 216 + <InteractiveFeature 217 + icon="eye-off" 218 + iconText="Restrict access" 219 + title="Password Protection." 220 + subTitle="Hide your page to unexepected users." 221 + component={ 222 + <div className="m-auto max-w-lg"> 223 + <PasswordFormSuspense slug="" /> 224 + </div> 225 + } 226 + col={2} 227 + position={props.position || "left"} 228 + /> 229 + ); 230 + } 231 + 232 + export function FeatureOperationalBanner( 233 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 234 + ) { 235 + return ( 236 + <InteractiveFeature 237 + icon="user" 238 + iconText="Keep it simple" 239 + title="Build trust." 240 + subTitle="Showcase your reliability to your users, and reduce the number of customer service tickets." 241 + component={<StatusCheck />} 242 + action={ 243 + <Button variant="outline" className="rounded-full w-max" asChild> 244 + <Link href="https://status.openstatus.dev">Status Page</Link> 245 + </Button> 246 + } 247 + col={2} 248 + position={props.position || "bottom"} 249 + /> 250 + ); 251 + } 252 + 253 + export function FeatureMaintenance( 254 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 255 + ) { 256 + return ( 257 + <InteractiveFeature 258 + icon="hammer" 259 + iconText="Handle migrations" 260 + title="Maintenance." 261 + subTitle="Mute your monitors for a specific period and inform the users about upcoming maintenance." 262 + component={ 263 + <div className="my-auto scale-[0.80]"> 264 + <MaintenanceContainer 265 + className="rounded-lg border-status-monitoring/10 bg-status-monitoring/5" 266 + {...maintenanceData} 267 + /> 268 + </div> 269 + } 270 + col={2} 271 + position={props.position || "left"} 272 + /> 273 + ); 274 + } 275 + 276 + export function FeatureRegions( 277 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 278 + ) { 279 + return ( 280 + <InteractiveFeature 281 + icon="activity" 282 + iconText="Website & API monitoring" 283 + title="Global Monitoring." 284 + subTitle="Get insights of the latency worldwide." 285 + component={ 286 + <Suspense fallback={<Skeleton />}> 287 + <div className="m-auto"> 288 + <RegionsPreset 289 + regions={flyRegions as unknown as Region[]} 290 + selectedRegions={flyRegions as unknown as Region[]} 291 + /> 292 + </div> 293 + </Suspense> 294 + } 295 + col={1} 296 + position={props.position || "left"} 297 + /> 298 + ); 299 + } 300 + 301 + export function FeatureResponseDetails( 302 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 303 + ) { 304 + return ( 305 + <InteractiveFeature 306 + icon="timer" 307 + iconText="Request Metrics Insights" 308 + title="Optimize Web Performance." 309 + subTitle="Analyze DNS, TCP, TLS, and TTFB for every request and inspect Response Headers as needed." 310 + component={ 311 + <div className="scale-[0.80] origin-top"> 312 + <Suspense fallback={<Skeleton />}> 313 + <ResponseDetailTabs 314 + {...mockResponseData} 315 + defaultOpen="timing" 316 + hideInfo={false} 317 + /> 318 + </Suspense> 319 + </div> 320 + } 321 + col={2} 322 + position={props.position || "left"} 323 + withGradient 324 + /> 325 + ); 326 + } 327 + 328 + export function FeatureRaycastIntegration( 329 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 330 + ) { 331 + return ( 332 + <InteractiveFeature 333 + icon="command" 334 + iconText="Raycast" 335 + title="Command K." 336 + subTitle="Check status pages and incidents without leaving your flow." 337 + component={<RaycastExample />} 338 + action={ 339 + <Button variant="outline" className="rounded-full w-max" asChild> 340 + <a 341 + href="https://www.raycast.com/thibaultleouay/openstatus" 342 + rel="noreferrer" 343 + target="_blank" 344 + > 345 + Raycast Store 346 + </a> 347 + </Button> 348 + } 349 + col={2} 350 + position={props.position || "left"} 351 + /> 352 + ); 353 + } 354 + 355 + const blockCICD = allUnrelateds.find( 356 + (unrelated) => unrelated.slug === "ci-cd-features-block", 357 + ); 358 + 359 + export function FeatureAPIMonitoring( 360 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 361 + ) { 362 + if (!blockCICD) { 363 + throw new Error("CI/CD block not found"); 364 + } 365 + 366 + return ( 367 + <InteractiveFeature 368 + icon="bot" 369 + iconText="API Monitoring" 370 + title="On Demand Checks." 371 + subTitle="Run your check in your CI/CD pipeline or via the CLI." 372 + component={ 373 + <Mdx 374 + code={blockCICD.mdx} 375 + className="max-w-none prose-pre:overflow-hidden" 376 + /> 377 + } 378 + action={ 379 + <Button variant="outline" className="rounded-full w-max" asChild> 380 + <a 381 + href="https://docs.openstatus.dev/cli/getting-started" 382 + rel="noreferrer" 383 + target="_blank" 384 + > 385 + Getting Started 386 + </a> 387 + </Button> 388 + } 389 + col={2} 390 + position={props.position || "bottom"} 391 + withGradient 392 + /> 393 + ); 394 + } 395 + 396 + const blockTerraform = allUnrelateds.find( 397 + (unrelated) => unrelated.slug === "terraform-provider-block", 398 + ); 399 + 400 + export function FeatureTerraformProvider( 401 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 402 + ) { 403 + if (!blockTerraform) { 404 + throw new Error("Terraform block not found"); 405 + } 406 + 407 + return ( 408 + <InteractiveFeature 409 + icon="bot" 410 + iconText="Terraform Provider" 411 + title="Infra as Code." 412 + subTitle="Use Terraform to manage your monitors." 413 + component={ 414 + <Mdx 415 + code={blockTerraform.mdx} 416 + className="max-w-none prose-pre:overflow-hidden" 417 + /> 418 + } 419 + action={ 420 + <Button variant="outline" className="rounded-full w-max" asChild> 421 + <a 422 + href="https://registry.terraform.io/providers/openstatusHQ/openstatus/latest" 423 + rel="noreferrer" 424 + target="_blank" 425 + > 426 + Terraform Registry 427 + </a> 428 + </Button> 429 + } 430 + col={2} 431 + position={props.position || "bottom"} 432 + withGradient 433 + /> 434 + ); 435 + } 436 + 437 + const blockGitHubAction = allUnrelateds.find( 438 + (unrelated) => unrelated.slug === "github-action-block", 439 + ); 440 + 441 + export function FeatureGitHubAction( 442 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 443 + ) { 444 + if (!blockGitHubAction) { 445 + throw new Error("GitHub Action block not found"); 446 + } 447 + 448 + return ( 449 + <InteractiveFeature 450 + icon="bot" 451 + iconText="GitHub Action" 452 + title="CI/CD pipeline." 453 + subTitle="Run your check on demand or in your workflows." 454 + className="max-h-max" 455 + component={ 456 + <Mdx 457 + code={blockGitHubAction.mdx} 458 + className="max-w-none prose-pre:overflow-hidden" 459 + /> 460 + } 461 + action={ 462 + <Button variant="outline" className="rounded-full w-max" asChild> 463 + <a 464 + href="https://github.com/marketplace/actions/openstatus-synthetics-ci" 465 + rel="noreferrer" 466 + target="_blank" 467 + > 468 + GitHub Action Marketplace 469 + </a> 470 + </Button> 471 + } 472 + col={2} 473 + position={props.position || "bottom"} 474 + /> 475 + ); 476 + } 477 + 478 + export function FeatureCLI( 479 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 480 + ) { 481 + const blockCLI = allUnrelateds.find( 482 + (unrelated) => unrelated.slug === "cli-block", 483 + ); 484 + 485 + if (!blockCLI) { 486 + throw new Error("CLI block not found"); 487 + } 488 + return ( 489 + <InteractiveFeature 490 + icon="terminal" 491 + iconText="CLI" 492 + title="Run everywhere." 493 + subTitle="Check your monitors from your favorite terminal." 494 + component={ 495 + <Mdx 496 + code={blockCLI.mdx} 497 + className="max-w-none prose-pre:overflow-hidden my-auto" 498 + /> 499 + } 500 + action={ 501 + <Button variant="outline" className="rounded-full w-max" asChild> 502 + <a 503 + href="https://docs.openstatus.dev/cli/getting-started" 504 + rel="noreferrer" 505 + target="_blank" 506 + > 507 + Getting Started 508 + </a> 509 + </Button> 510 + } 511 + col={2} 512 + position={props.position || "right"} 513 + /> 514 + ); 515 + } 516 + 517 + const blockOpenTelemetry = allUnrelateds.find( 518 + (unrelated) => unrelated.slug === "otel-block", 519 + ); 520 + 521 + export function FeatureOpenTelemetry( 522 + props: Partial<Pick<InteractiveFeatureProps, "position">>, 523 + ) { 524 + if (!blockOpenTelemetry) { 525 + throw new Error("OpenTelemetry block not found"); 526 + } 527 + 528 + return ( 529 + <InteractiveFeature 530 + icon="radar" 531 + iconText="OpenTelemetry" 532 + title="Track every request." 533 + subTitle="Visualize how requests flow through your system using OpenTelemetry tracing." 534 + component={ 535 + <Mdx 536 + code={blockOpenTelemetry.mdx} 537 + className="max-w-none prose-pre:overflow-hidden" 538 + /> 539 + } 540 + action={ 541 + <Button variant="outline" className="rounded-full w-max" asChild> 542 + <a 543 + href="https://docs.openstatus.dev/monitoring/opentelemetry" 544 + rel="noreferrer" 545 + target="_blank" 546 + > 547 + Documentation 548 + </a> 549 + </Button> 550 + } 551 + col={2} 552 + position={props.position || "top"} 553 + withGradient 554 + /> 555 + ); 556 + }
+40
apps/web/src/components/marketing/feature/notifications-form-example.tsx
··· 1 + "use client"; 2 + 3 + import { SectionNotifications } from "@/components/forms/monitor/section-notifications"; 4 + import { zodResolver } from "@hookform/resolvers/zod"; 5 + import { 6 + type InsertMonitor, 7 + type Notification, 8 + insertMonitorSchema, 9 + } from "@openstatus/db/src/schema"; 10 + import { Form } from "@openstatus/ui"; 11 + import { useForm } from "react-hook-form"; 12 + 13 + const notifications: Notification[] = ( 14 + [ 15 + { name: "Team", provider: "email" }, 16 + { name: "#alerts", provider: "discord" }, 17 + { name: "#alerts", provider: "slack" }, 18 + ] as const 19 + ).map((n, i) => ({ 20 + ...n, 21 + id: i, 22 + createdAt: new Date(), 23 + updatedAt: new Date(), 24 + workspaceId: 1, 25 + data: "", 26 + })); 27 + 28 + export function NotificationsFormExample() { 29 + const form = useForm<InsertMonitor>({ 30 + resolver: zodResolver(insertMonitorSchema), 31 + defaultValues: { 32 + notifications: [0], 33 + }, 34 + }); 35 + return ( 36 + <Form {...form}> 37 + <SectionNotifications form={form} notifications={notifications} /> 38 + </Form> 39 + ); 40 + }
+43
apps/web/src/components/marketing/feature/raycast-example.tsx
··· 1 + import { 2 + Command, 3 + CommandEmpty, 4 + CommandGroup, 5 + CommandInput, 6 + CommandItem, 7 + CommandList, 8 + } from "@openstatus/ui"; 9 + import Image from "next/image"; 10 + 11 + const commands = [ 12 + "Show Monitors", 13 + "Create Status Report", 14 + "Show Status Page", 15 + "Create Status Report Update", 16 + "Show Incidents", 17 + ]; 18 + 19 + export function RaycastExample() { 20 + return ( 21 + <Command className="rounded-lg border shadow-md md:min-w-[450px]"> 22 + <CommandInput placeholder="Search for apps and commands..." /> 23 + <CommandList> 24 + <CommandEmpty>No results found.</CommandEmpty> 25 + <CommandGroup heading="Commands"> 26 + {commands.map((command) => ( 27 + <CommandItem key={command}> 28 + <span className="relative mr-2"> 29 + <Image 30 + src="/icon.png" 31 + alt="OpenStatus" 32 + width={16} 33 + height={16} 34 + /> 35 + </span> 36 + <span>{command}</span> 37 + </CommandItem> 38 + ))} 39 + </CommandGroup> 40 + </CommandList> 41 + </Command> 42 + ); 43 + }
+101
apps/web/src/config/landings.tsx
··· 1 + import type { ValidIcon } from "@/components/icons"; 2 + import { EnterpriseBanner } from "@/components/marketing/banner/enterprise-banner"; 3 + import { 4 + BookingBanner, 5 + FeatureAPIMonitoring, 6 + FeatureCLI, 7 + FeatureCharts, 8 + FeatureCustomDomain, 9 + FeatureGitHubAction, 10 + FeatureMaintenance, 11 + FeatureNotifications, 12 + FeatureOpenTelemetry, 13 + FeatureOperationalBanner, 14 + FeaturePasswordProtection, 15 + FeatureRaycastIntegration, 16 + FeatureRegions, 17 + FeatureResponseDetails, 18 + FeatureStatusPageTracker, 19 + FeatureStatusPageTrackerToggle, 20 + FeatureStatusUpdates, 21 + FeatureSubscriptions, 22 + FeatureTerraformProvider, 23 + FeatureTimingAssertions, 24 + SpeedBanner, 25 + } from "@/components/marketing/feature"; 26 + 27 + type Landing = { 28 + title: string; 29 + description: string; 30 + icon: ValidIcon; 31 + blocks: React.ReactNode[]; 32 + }; 33 + 34 + export const landingsConfig = { 35 + "uptime-monitoring": { 36 + icon: "activity", 37 + title: "Uptime Monitoring", 38 + description: 39 + "Monitor your uptime and get notified when your services are down.", 40 + blocks: [ 41 + <FeatureNotifications key="feature-notifications" />, 42 + <SpeedBanner key="speed-banner" />, 43 + <FeatureTimingAssertions key="feature-timing-assertions" />, 44 + <FeatureStatusPageTracker key="feature-status-page-tracker" />, 45 + <FeatureCharts key="feature-charts" />, 46 + <FeatureRaycastIntegration key="feature-raycast-integration" />, 47 + <BookingBanner key="booking-banner" />, 48 + ], 49 + }, 50 + "status-page": { 51 + icon: "panel-top", 52 + title: "Status Page", 53 + description: "Create a status page to inform your users about the uptime.", 54 + blocks: [ 55 + <FeatureCustomDomain key="feature-custom-domain" />, 56 + <FeatureStatusPageTrackerToggle key="feature-status-page-tracker" />, 57 + <FeatureSubscriptions key="feature-subscriptions" />, 58 + <FeatureStatusUpdates key="feature-status-updates" />, 59 + <FeaturePasswordProtection key="feature-password-protection" />, 60 + <FeatureOperationalBanner key="feature-operational-banner" />, 61 + <FeatureMaintenance key="feature-maintenance" />, 62 + <FeatureRaycastIntegration 63 + key="feature-raycast-integration" 64 + position="right" 65 + />, 66 + <BookingBanner key="booking-banner" />, 67 + ], 68 + }, 69 + "synthetic-monitoring": { 70 + icon: "network", 71 + title: "Synthetic Monitoring", 72 + description: "Proactively monitor your api and website globally.", 73 + blocks: [ 74 + <FeatureRegions key="feature-regions" />, 75 + <SpeedBanner key="speed-banner" />, 76 + <FeatureTimingAssertions key="feature-timing-assertions" />, 77 + <FeatureAPIMonitoring key="feature-api-monitoring" />, 78 + <FeatureResponseDetails 79 + key="feature-response-details" 80 + position="right" 81 + />, 82 + <FeatureTerraformProvider key="feature-terraform-provider" />, 83 + <BookingBanner key="booking-banner" />, 84 + ], 85 + }, 86 + "ci-cd": { 87 + icon: "terminal", 88 + title: "CI/CD Testing", 89 + description: 90 + "Run your synthetic checks in your CI/CD pipeline and export metrics to your observability stack.", 91 + blocks: [ 92 + <FeatureGitHubAction key="feature-github-action" />, 93 + <EnterpriseBanner key="enterprise-banner" />, 94 + <FeatureAPIMonitoring key="feature-api-monitoring" />, 95 + <FeatureCLI key="feature-cli" />, 96 + <FeatureTerraformProvider key="feature-terraform-provider" />, 97 + <FeatureOpenTelemetry key="feature-open-telemetry" />, 98 + <BookingBanner key="booking-banner" />, 99 + ], 100 + }, 101 + } satisfies Record<string, Landing>;
+17 -22
apps/web/src/config/pages.ts
··· 1 1 import type { ValidIcon } from "@/components/icons"; 2 2 3 + import { landingsConfig } from "./landings"; 4 + 3 5 export type Page = { 4 6 title: string; 5 7 subtitle?: string; ··· 203 205 type MarketingPageType = Page; 204 206 205 207 export const marketingProductPagesConfig = [ 206 - { 207 - href: "/features/monitoring", 208 - title: "Synthetic Monitoring", 209 - subtitle: 210 - "Get insights of the latency of your API and website from all over the world.", 211 - description: "Monitor your API and website globally.", 212 - segment: "features", 213 - icon: "activity", 214 - }, 215 - { 216 - href: "/features/status-page", 217 - title: "Status Page", 218 - subtitle: 219 - "Easily report to your users with our public or private status page.", 220 - description: "Create beautiful status pages within seconds.", 221 - segment: "features", 222 - icon: "panel-top", 223 - }, 208 + ...Object.entries(landingsConfig).map( 209 + ([key, { title, description, icon }]) => { 210 + return { 211 + title, 212 + href: `/${key}`, 213 + description, 214 + icon, 215 + segment: "product", 216 + }; 217 + }, 218 + ), 224 219 ] as const satisfies MarketingPageType[]; 225 220 226 221 export const marketingResourcePagesConfig = [ ··· 247 242 }, 248 243 { 249 244 href: "/compare", 250 - title: "Alternatives", 251 - description: "Compare OpenStatus with other services.", 252 - segment: "alternative", 245 + title: "Compare", 246 + description: "Discover how OpenStatus compares to other services.", 247 + segment: "compare", 253 248 icon: "compare", 254 249 }, 255 250 ] as const satisfies Page[]; ··· 259 254 href: "/product", 260 255 title: "Product", 261 256 description: "All product features for OpenStatus", 262 - segment: "", 257 + segment: "product", 263 258 icon: "package", 264 259 children: marketingProductPagesConfig, 265 260 },
+6 -12
apps/web/src/content/unrelated/ci-cd-features-block.mdx
··· 1 1 ```ts 2 2 test("should fail if slow", async () => { 3 - const data = await fetch("https://api.openstatus.dev/v1/check", { 3 + const data = await fetch("https://api.openstatus.dev/v1/monitor/:id/run", { 4 4 method: "POST", 5 - headers: { 6 - "Content-Type": "application/json", 7 - "x-openstatus-key": process.env.OPENSTATUS_API_KEY, 8 - }, 9 - body: JSON.stringify({ 10 - url: "https://openstat.us", 11 - method: "GET", 12 - regions: ["ams", "iad", "gru"], 13 - runCount: 5, 14 - aggregated: true, 15 - }), 5 + headers: { "x-openstatus-key": process.env.OPENSTATUS_API_KEY }, 16 6 }); 7 + const body = await data.json(); 8 + 9 + expect(body.status).toBe(200); 10 + expect(body.latency).toBeLessThan(1000); 17 11 }); 18 12 ```
+3
apps/web/src/content/unrelated/cli-block.mdx
··· 1 + ```bash 2 + openstatus monitors trigger [id] 3 + ```
+20
apps/web/src/content/unrelated/github-action-block.mdx
··· 1 + ```yaml 2 + name: Run OpenStatus Synthetics CI 3 + 4 + on: 5 + workflow_dispatch: 6 + push: 7 + branches: [main] 8 + 9 + jobs: 10 + synthetic_ci: 11 + runs-on: ubuntu-latest 12 + name: Run OpenStatus Synthetics CI 13 + steps: 14 + - name: Checkout 15 + uses: actions/checkout@v4 16 + - name: Run OpenStatus Synthetics CI 17 + uses: openstatushq/openstatus-github-action@v1 18 + with: 19 + api_key: ${{ secrets.OPENSTATUS_API_KEY }} 20 + ```
+82
apps/web/src/content/unrelated/otel-block.mdx
··· 1 + ```json 2 + { 3 + "resourceMetrics": [ 4 + { 5 + "resource": { 6 + "attributes": [ 7 + { 8 + "key": "service.name", 9 + "value": { "stringValue": "openstatus-synthetic-check" } 10 + }, 11 + { 12 + "key": "service.version", 13 + "value": { "stringValue": "0.1.0" } 14 + } 15 + ] 16 + }, 17 + "scopeMetrics": [ 18 + { 19 + "scope": { 20 + "name": "OpenStatus" 21 + }, 22 + "metrics": [ 23 + { 24 + "name": "openstatus.status", 25 + "description": "Status of the check", 26 + "unit": "1", 27 + "sum": { 28 + "dataPoints": [ 29 + { 30 + "attributes": [ 31 + { "key": "openstatus.probes", "value": { "stringValue": "fra" } }, 32 + { "key": "openstatus.target", "value": { "stringValue": "https://acme.com" } }, 33 + { "key": "http.response.status_code", "value": { "intValue": 200 } } 34 + ], 35 + "value": 1 36 + } 37 + ], 38 + "aggregationTemporality": "AGGREGATION_TEMPORALITY_CUMULATIVE", 39 + "isMonotonic": true 40 + } 41 + }, 42 + { 43 + "name": "openstatus.http.request.duration", 44 + "description": "Duration of the check", 45 + "unit": "ms", 46 + "gauge": { 47 + "dataPoints": [ 48 + { 49 + "attributes": [ 50 + { "key": "openstatus.probes", "value": { "stringValue": "fra" } }, 51 + { "key": "openstatus.target", "value": { "stringValue": "https://acme.com" } }, 52 + { "key": "http.response.status_code", "value": { "intValue": 200 } } 53 + ], 54 + "value": 431.12 55 + } 56 + ] 57 + } 58 + }, 59 + { 60 + "name": "openstatus.http.dns.duration", 61 + "description": "Duration of the dns lookup", 62 + "unit": "ms", 63 + "gauge": { 64 + "dataPoints": [ 65 + { 66 + "attributes": [ 67 + { "key": "openstatus.probes", "value": { "stringValue": "fra" } }, 68 + { "key": "openstatus.target", "value": { "stringValue": "https://acme.com" } }, 69 + { "key": "http.response.status_code", "value": { "intValue": 200 } } 70 + ], 71 + "value": 56.45 72 + } 73 + ] 74 + } 75 + } 76 + ] 77 + } 78 + ] 79 + } 80 + ] 81 + } 82 + ```
+14
apps/web/src/content/unrelated/terraform-provider-block.mdx
··· 1 + ```hcl 2 + provider "openstatus" { 3 + openstatus_api_token= "YOUR_API_TOKEN" 4 + } 5 + 6 + resource "openstatus_monitor" "my_monitor" { 7 + url = "https://www.openstatus.dev" 8 + regions= ["iad", "jnb"] 9 + periodicity = "10m" 10 + name = "test-monitor" 11 + active = true 12 + description = "This is a test monitor" 13 + } 14 + ```