diff --git a/apps/web/public/og.png b/apps/web/public/og.png new file mode 100644 index 0000000000..3c1c9160e1 Binary files /dev/null and b/apps/web/public/og.png differ diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index 3426058e24..be9e08bd34 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -5,15 +5,6 @@ import Link from "next/link"; import Balancer from "react-wrap-balancer"; import Logo from "../components/logo"; -export const metadata: Metadata = { - title: "Comp AI - Get SOC 2, ISO 27001 and GDPR compliant", - description: - "The open-source compliance automation platform for SOC 2, ISO 27001, GDPR and more.", - alternates: { - canonical: "https://trycomp.ai", - }, -}; - export default function Home() { return (
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 806438cef8..0223e9911c 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -4,17 +4,14 @@ import { Toaster } from "sonner"; import "@bubba/ui/globals.css"; import { Providers } from "@/app/providers"; import { env } from "@/env.mjs"; +import { generatePageMeta } from "@/lib/seo"; import { cn } from "@/lib/utils"; import { GoogleTagManager } from "@next/third-parties/google"; import localFont from "next/font/local"; -export const metadata: Metadata = { - title: "Comp AI", - description: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - icons: { - icon: "/favicon-96x96.png", - }, -}; +export const metadata = generatePageMeta({ + url: "/", +}); const font = localFont({ src: "../../public/fonts/GeneralSans-Variable.ttf", diff --git a/apps/web/src/lib/seo.tsx b/apps/web/src/lib/seo.tsx new file mode 100644 index 0000000000..eb3a8b6bff --- /dev/null +++ b/apps/web/src/lib/seo.tsx @@ -0,0 +1,158 @@ +import type { Metadata } from "next"; +import type { OpenGraph } from "next/dist/lib/metadata/types/opengraph-types"; +import type { Twitter } from "next/dist/lib/metadata/types/twitter-types"; +import type { StaticImageData } from "next/image"; + +const baseURL = "https://trycomp.ai"; +const title = "Comp AI - Open Source Platform for SOC 2, ISO 27001 & GDPR"; +const description = + "The Open Source Drata & Vanta alternative that does everything you need to get compliant, fast. Get SOC 2, ISO 27001 and GDPR compliant in minutes."; + +export const rootOpenGraph: OpenGraph = { + locale: "en", + type: "website", + url: baseURL, + siteName: "Comp AI", + title, + description, +}; + +export const rootTwitter: Twitter = { + title, + description, + card: "summary_large_image", + creator: "@trycompai", + site: "@trycompai", +}; + +export const rootMetadata: Metadata = { + metadataBase: new URL(baseURL), + title, + description, + applicationName: "Comp AI", + openGraph: rootOpenGraph, + twitter: rootTwitter, + robots: + "follow, index, max-snippet:-1, max-video-preview:-1, max-image-preview:large", +}; + +function getImage( + image?: StaticImageData | string, + alt?: string, + width?: number, + height?: number, +) { + if (!image) { + return null; + } + + if (typeof image === "string") { + return { + url: image, + alt, + width, + height, + type: image.endsWith(".png") ? "image/png" : "image/jpeg", + }; + } + + return { + url: image.src, + width: image.width, + height: image.height, + alt, + type: image.src.endsWith(".png") ? "image/png" : "image/jpeg", + }; +} + +export function generatePageMeta({ + title = rootMetadata.title as string, + description = rootMetadata.description as string, + url, + image, + image_alt, + image_width, + image_height, + publishedAt, + updatedAt, + siteName = rootMetadata.applicationName as string, + authors, + noindex, + locale = "en", +}: { + title?: string; + description?: string; + url?: string; + image?: StaticImageData | string; + image_alt?: string; + image_width?: number; + image_height?: number; + publishedAt?: string; + updatedAt?: string; + authors?: string[]; + siteName?: string; + noindex?: boolean; + locale?: string; +} = {}): Metadata { + const metadata = { + ...rootMetadata, + title, + description, + alternates: { + canonical: url, + }, + authors: authors, + openGraph: { + ...rootOpenGraph, + locale, + url, + title: title, + description, + } as OpenGraph, + twitter: { + ...rootTwitter, + title: title, + description, + } as Twitter, + publisher: siteName, + other: {}, + } as Metadata; + + if (publishedAt) { + metadata.openGraph = { + ...metadata.openGraph, + type: "article", + locale: locale, + publishedTime: publishedAt, + modifiedTime: updatedAt ?? publishedAt, + authors: authors ?? [], + section: siteName, + tags: [siteName ?? "Comp AI"], + }; + } + + const img = getImage(image, image_alt ?? title, image_width, image_height); + const screenshot = { + url: `${metadata.metadataBase}og.png`, + width: 1200, + height: 630, + alt: title, + type: "image/png", + }; + metadata.openGraph!.images = img ? [img] : [screenshot]; + metadata.twitter!.images = img ? [img] : [screenshot]; + + if (siteName) { + metadata.applicationName = siteName; + metadata.openGraph!.siteName = siteName; + } + + if (noindex) { + metadata.robots = { + index: false, + follow: true, + }; + } + + return metadata; +}