import type { Metadata } from "next"; import type { MDXData } from "../../content/utils"; export const TITLE = "openstatus"; export const HOMEPAGE_TITLE = "Free & Open Source Status Page and Uptime Monitoring"; export const DESCRIPTION = "Open source status page and uptime monitoring. Your monitors update it, your subscribers hear it from you first. Free to start, self-hostable."; export const OG_DESCRIPTION = "The status page for humans and agents"; export const BASE_URL = process.env.NODE_ENV === "production" ? "https://www.openstatus.dev" : "http://localhost:3000"; export const APP_URL = "https://app.openstatus.dev"; export const twitterMetadata: Metadata["twitter"] = { title: TITLE, description: DESCRIPTION, card: "summary_large_image", images: ["/api/og"], }; export const ogMetadata: Metadata["openGraph"] = { title: TITLE, description: DESCRIPTION, type: "website", images: ["/api/og"], }; export const defaultMetadata: Metadata = { title: { template: `%s | ${TITLE}`, default: HOMEPAGE_TITLE, }, description: DESCRIPTION, metadataBase: new URL(BASE_URL), alternates: { canonical: "/", }, twitter: twitterMetadata, openGraph: ogMetadata, }; // Next merges metadata shallowly per field, so a page must emit its own // og/twitter blocks or it inherits the root layout's generic ones wholesale. export const getSocialMetadata = (args: { title: string; description: string; url: string; category?: string; ogImage?: string; }): Pick => { const { title, description, url, category, ogImage: ogImageOverride } = args; const searchParams = new URLSearchParams({ title, description }); if (category) searchParams.set("category", category); const ogImage = ogImageOverride ?? `${BASE_URL}/api/og?${searchParams.toString()}`; return { openGraph: { title, description, type: "website", url, images: [ { url: ogImage, }, ], }, twitter: { card: "summary_large_image", title, description, images: [ogImage], }, }; }; export const getPageMetadata = (page: MDXData, basePath?: string): Metadata => { const { slug, metadata } = page; const { title, description, category, publishedAt, updatedAt, seo } = metadata; const url = basePath ? `${BASE_URL}/${basePath}/${slug}` : `${BASE_URL}/${slug}`; const metaTitle = seo?.title ?? title; const metaDescription = seo?.description ?? description; const { openGraph, twitter } = getSocialMetadata({ title: metaTitle, description: metaDescription, url, category, ogImage: seo?.ogImage, }); return { // `absolute` bypasses the `%s | openstatus` template so the override is verbatim title: seo?.title ? { absolute: seo.title } : title, description: metaDescription, alternates: { canonical: seo?.canonical ?? url, }, ...(seo?.noindex ? { robots: { index: false } } : {}), openGraph: { ...openGraph, type: "article", publishedTime: publishedAt.toISOString(), modifiedTime: (updatedAt ?? publishedAt).toISOString(), }, twitter, }; }; // Home sits outside `getPageMetadata`: its canonical is `/`, its title skips // the `%s | openstatus` template and its OG card is the static one. export const getHomeMetadata = (page: MDXData): Metadata => { const { title, description, category, seo } = page.metadata; const metaTitle = seo?.title ?? title; const metaDescription = seo?.description ?? description; return { ...defaultMetadata, title: { absolute: metaTitle }, description: metaDescription, alternates: { canonical: seo?.canonical ?? "/", }, ...(seo?.noindex ? { robots: { index: false } } : {}), ...getSocialMetadata({ title: metaTitle, description: metaDescription, url: BASE_URL, category, ogImage: seo?.ogImage ?? `${BASE_URL}/api/og`, }), }; };