diff --git a/CHANGELOG.md b/CHANGELOG.md index b3aa45e..a86cd8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +#### Open Graph & Twitter link previews + +- Added `metadataBase`, `openGraph`, and `twitter` metadata to the web app's + root layout so shared links render a proper social card instead of the + auth-gated app's "404: This page could not be found." +- Added `apps/web/app/opengraph-image.tsx` (and a re-exporting + `twitter-image.tsx`) that generate a branded 1200×630 preview card at + build/request time via `next/og` — no static asset to maintain. +- Marked `/opengraph-image` and `/twitter-image` as public routes in + `proxy.ts` so crawlers can fetch the preview image without authentication. + ### Changed #### Production widget URL & dashboard entry point diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 302f315..642363c 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -16,13 +16,20 @@ const fontMono = Geist_Mono({ variable: "--font-mono", }) +const siteUrl = + process.env.NEXT_PUBLIC_APP_URL ?? "https://echo-red-kappa.vercel.app" + +const siteTitle = "Echo — AI-Powered Customer Support Platform" +const siteDescription = + "Echo is a B2B AI-powered customer support platform. Deploy an embeddable chat and voice widget, resolve conversations with an AI agent grounded in your knowledge base, and manage everything from a real-time operator dashboard." + export const metadata: Metadata = { + metadataBase: new URL(siteUrl), title: { - default: "Echo — AI-Powered Customer Support Platform", + default: siteTitle, template: "%s · Echo", }, - description: - "Echo is a B2B AI-powered customer support platform. Deploy an embeddable chat and voice widget, resolve conversations with an AI agent grounded in your knowledge base, and manage everything from a real-time operator dashboard.", + description: siteDescription, applicationName: "Echo", keywords: [ "AI customer support", @@ -35,6 +42,18 @@ export const metadata: Metadata = { icons: { icon: "/icon.svg", }, + openGraph: { + type: "website", + url: siteUrl, + siteName: "Echo", + title: siteTitle, + description: siteDescription, + }, + twitter: { + card: "summary_large_image", + title: siteTitle, + description: siteDescription, + }, } export default function RootLayout({ diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx new file mode 100644 index 0000000..7697ea3 --- /dev/null +++ b/apps/web/app/opengraph-image.tsx @@ -0,0 +1,98 @@ +import { ImageResponse } from "next/og" + +// Route segment config +export const alt = "Echo — AI-Powered Customer Support Platform" +export const size = { width: 1200, height: 630 } +export const contentType = "image/png" + +// Branded Open Graph card, generated at build/request time so shared links +// (LinkedIn, X, Slack, etc.) always render a proper preview instead of the +// auth-gated app's 404. +export default function OpengraphImage() { + return new ImageResponse( +
+
+
+ E +
+
Echo
+
+ +
+ AI-Powered Customer Support Platform +
+ +
+ Embeddable chat & voice widget · RAG-grounded AI agent · real-time + human takeover — installed with one line of code. +
+ +
+ {["Next.js", "Convex", "Gemini", "Vapi", "Clerk"].map((tech) => ( +
+ {tech} +
+ ))} +
+
, + { ...size } + ) +} diff --git a/apps/web/app/twitter-image.tsx b/apps/web/app/twitter-image.tsx new file mode 100644 index 0000000..b5d85e3 --- /dev/null +++ b/apps/web/app/twitter-image.tsx @@ -0,0 +1,3 @@ +// Reuse the same branded card for Twitter/X so `twitter:image` is populated +// alongside `og:image`. +export { default, alt, size, contentType } from "./opengraph-image" diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index acacdda..e04ca38 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -1,12 +1,20 @@ import { NextResponse } from "next/server" import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" -const isPublicRoute = createRouteMatcher(["/sign-in(.*)", "/sign-up(.*)"]) +const isPublicRoute = createRouteMatcher([ + "/sign-in(.*)", + "/sign-up(.*)", + // Social-share preview assets must be crawlable without authentication. + "/opengraph-image(.*)", + "/twitter-image(.*)", +]) const isOrgFreeRoute = createRouteMatcher([ "/sign-in(.*)", "/sign-up(.*)", "/org-selection(.*)", + "/opengraph-image(.*)", + "/twitter-image(.*)", ]) export default clerkMiddleware(async (auth, req) => {