Openstatus www.openstatus.dev

chore: stpg improvements (#1362)

* refactor: metadata

* fix: favicon

* wip: support button

* chore: page links

* fix: format

authored by

Maximilian Kaske and committed by
GitHub
762630dd 22132c19

+133 -71
-49
apps/status-page/src/app/(status-page)/[domain]/(public)/layout.tsx
··· 1 - import { defaultMetadata, ogMetadata, twitterMetadata } from "@/app/metadata"; 2 1 import { Footer } from "@/components/nav/footer"; 3 2 import { Header } from "@/components/nav/header"; 4 - import { getQueryClient, trpc } from "@/lib/trpc/server"; 5 - import type { Metadata } from "next"; 6 - import { notFound } from "next/navigation"; 7 3 8 4 export default function Layout({ children }: { children: React.ReactNode }) { 9 5 return ( ··· 16 12 </div> 17 13 ); 18 14 } 19 - 20 - export async function generateMetadata({ 21 - params, 22 - }: { 23 - params: Promise<{ domain: string }>; 24 - }): Promise<Metadata> { 25 - const queryClient = getQueryClient(); 26 - const { domain } = await params; 27 - const page = await queryClient.fetchQuery( 28 - trpc.statusPage.get.queryOptions({ slug: domain }), 29 - ); 30 - 31 - if (!page) return notFound(); 32 - 33 - return { 34 - ...defaultMetadata, 35 - title: { 36 - template: `%s | ${page.title}`, 37 - default: page?.title, 38 - }, 39 - description: page?.description, 40 - icons: page?.icon, 41 - alternates: { 42 - canonical: page?.customDomain 43 - ? `https://${page.customDomain}` 44 - : `https://${page.slug}.openstatus.dev`, 45 - }, 46 - twitter: { 47 - ...twitterMetadata, 48 - images: [ 49 - `/api/og/page?slug=${page?.slug}&passwordProtected=${page?.passwordProtected}`, 50 - ], 51 - title: page?.title, 52 - description: page?.description, 53 - }, 54 - openGraph: { 55 - ...ogMetadata, 56 - images: [ 57 - `/api/og/page?slug=${page?.slug}&passwordProtected=${page?.passwordProtected}`, 58 - ], 59 - title: page?.title, 60 - description: page?.description, 61 - }, 62 - }; 63 - }
+48
apps/status-page/src/app/(status-page)/[domain]/layout.tsx
··· 1 + import { defaultMetadata, ogMetadata, twitterMetadata } from "@/app/metadata"; 1 2 import { 2 3 FloatingButton, 3 4 StatusPageProvider, ··· 5 6 import { ThemeProvider } from "@/components/theme-provider"; 6 7 import { Toaster } from "@/components/ui/sonner"; 7 8 import { HydrateClient, getQueryClient, trpc } from "@/lib/trpc/server"; 9 + import type { Metadata } from "next"; 10 + import { notFound } from "next/navigation"; 8 11 import { z } from "zod"; 9 12 10 13 export const schema = z.object({ ··· 55 58 </HydrateClient> 56 59 ); 57 60 } 61 + 62 + export async function generateMetadata({ 63 + params, 64 + }: { 65 + params: Promise<{ domain: string }>; 66 + }): Promise<Metadata> { 67 + const queryClient = getQueryClient(); 68 + const { domain } = await params; 69 + const page = await queryClient.fetchQuery( 70 + trpc.statusPage.get.queryOptions({ slug: domain }), 71 + ); 72 + 73 + if (!page) return notFound(); 74 + 75 + return { 76 + ...defaultMetadata, 77 + title: { 78 + template: `%s | ${page.title}`, 79 + default: page?.title, 80 + }, 81 + description: page?.description, 82 + icons: page?.icon, 83 + alternates: { 84 + canonical: page?.customDomain 85 + ? `https://${page.customDomain}` 86 + : `https://${page.slug}.openstatus.dev`, 87 + }, 88 + twitter: { 89 + ...twitterMetadata, 90 + images: [ 91 + `/api/og/page?slug=${page?.slug}&passwordProtected=${page?.passwordProtected}`, 92 + ], 93 + title: page?.title, 94 + description: page?.description, 95 + }, 96 + openGraph: { 97 + ...ogMetadata, 98 + images: [ 99 + `/api/og/page?slug=${page?.slug}&passwordProtected=${page?.passwordProtected}`, 100 + ], 101 + title: page?.title, 102 + description: page?.description, 103 + }, 104 + }; 105 + }
apps/status-page/src/app/apple.ico

This is a binary file and will not be displayed.

apps/status-page/src/app/favicon.ico

This is a binary file and will not be displayed.

+1
apps/status-page/src/app/metadata.ts
··· 14 14 template: `%s | ${TITLE}`, 15 15 default: TITLE, 16 16 }, 17 + icons: "https://www.openstatus.dev/favicon.ico", 17 18 description: DESCRIPTION, 18 19 metadataBase: new URL("https://www.openstatus.dev"), 19 20 };
+1 -1
apps/status-page/src/components/nav/footer.tsx
··· 20 20 21 21 return ( 22 22 <footer {...props}> 23 - <div className="mx-auto flex gap-4 max-w-2xl items-center justify-between px-3 py-2"> 23 + <div className="mx-auto flex max-w-2xl items-center justify-between gap-4 px-3 py-2"> 24 24 <div className="leading-[0.9]"> 25 25 <p 26 26 className={cn(
+83 -21
apps/status-page/src/components/nav/header.tsx
··· 10 10 SheetTitle, 11 11 SheetTrigger, 12 12 } from "@/components/ui/sheet"; 13 + import { 14 + Tooltip, 15 + TooltipContent, 16 + TooltipProvider, 17 + TooltipTrigger, 18 + } from "@/components/ui/tooltip"; 13 19 import { usePathnamePrefix } from "@/hooks/use-pathname-prefix"; 14 20 import { useTRPC } from "@/lib/trpc/client"; 15 21 import { cn } from "@/lib/utils"; 16 22 import { useMutation, useQuery } from "@tanstack/react-query"; 17 - import { Menu } from "lucide-react"; 23 + import { Menu, MessageCircleMore } from "lucide-react"; 18 24 import NextLink from "next/link"; 19 25 import { useParams, usePathname } from "next/navigation"; 20 26 import { useState } from "react"; ··· 62 68 }), 63 69 ); 64 70 71 + const hasSubscribers = page?.workspacePlan !== "free"; 72 + 65 73 const types = ( 66 74 page?.workspacePlan === "free" ? ["rss", "atom"] : ["email", "rss", "atom"] 67 75 ) satisfies ("email" | "rss" | "atom")[]; ··· 70 78 <header {...props}> 71 79 <nav className="mx-auto flex max-w-2xl items-center justify-between gap-3 px-3 py-2"> 72 80 {/* NOTE: same width as the `StatusUpdates` button */} 73 - <div className="w-[105px] shrink-0"> 74 - <Link href="/"> 81 + <div className="w-[150px] shrink-0"> 82 + <Link 83 + href={page?.homepageUrl ?? "/"} 84 + target={page?.homepageUrl ? "_blank" : undefined} 85 + rel={page?.homepageUrl ? "noreferrer" : undefined} 86 + className="rounded-full" 87 + > 75 88 {page?.icon ? ( 76 89 <img 77 90 src={page.icon} ··· 82 95 </Link> 83 96 </div> 84 97 <NavDesktop className="hidden md:flex" /> 85 - <StatusUpdates 86 - className="hidden md:block" 87 - types={types} 88 - onSubscribe={async (email) => { 89 - await subscribeMutation.mutateAsync({ slug: domain, email }); 90 - }} 91 - slug={page?.slug} 92 - /> 93 - <div className="flex gap-3 md:hidden"> 94 - <NavMobile /> 95 - <StatusUpdates 96 - types={types} 97 - onSubscribe={async (email) => { 98 - await subscribeMutation.mutateAsync({ slug: domain, email }); 99 - }} 100 - slug={page?.slug} 101 - /> 98 + <div className="flex min-w-[150px] items-center justify-end gap-2"> 99 + {page?.contactUrl ? ( 100 + <GetInTouch 101 + buttonType={!hasSubscribers ? "text" : "icon"} 102 + link={page.contactUrl} 103 + /> 104 + ) : null} 105 + {hasSubscribers ? ( 106 + <StatusUpdates 107 + types={types} 108 + onSubscribe={async (email) => { 109 + await subscribeMutation.mutateAsync({ slug: domain, email }); 110 + }} 111 + slug={page?.slug} 112 + /> 113 + ) : null} 114 + <NavMobile className="md:hidden" /> 102 115 </div> 103 116 </nav> 104 117 </header> ··· 138 151 <Button 139 152 variant="secondary" 140 153 size="sm" 141 - className={cn("size-8", className)} 154 + className={cn("size-8 border", className)} 142 155 {...props} 143 156 > 144 157 <Menu /> ··· 171 184 </Sheet> 172 185 ); 173 186 } 187 + 188 + function GetInTouch({ 189 + buttonType, 190 + className, 191 + link, 192 + ...props 193 + }: React.ComponentProps<typeof Button> & { 194 + buttonType: "icon" | "text"; 195 + link: string; 196 + }) { 197 + if (buttonType === "text") { 198 + return ( 199 + <Button 200 + variant="outline" 201 + size="sm" 202 + type="button" 203 + className={className} 204 + asChild 205 + {...props} 206 + > 207 + <a href={link} target="_blank" rel="noreferrer"> 208 + Get in touch 209 + </a> 210 + </Button> 211 + ); 212 + } 213 + return ( 214 + <TooltipProvider> 215 + <Tooltip> 216 + <TooltipTrigger asChild> 217 + <Button 218 + variant="ghost" 219 + size="icon" 220 + type="button" 221 + className={cn("size-8", className)} 222 + {...props} 223 + > 224 + <a href={link} target="_blank" rel="noreferrer"> 225 + <MessageCircleMore /> 226 + </a> 227 + </Button> 228 + </TooltipTrigger> 229 + <TooltipContent> 230 + <p>Get in touch</p> 231 + </TooltipContent> 232 + </Tooltip> 233 + </TooltipProvider> 234 + ); 235 + }