Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 22 additions & 3 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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({
Expand Down
98 changes: 98 additions & 0 deletions apps/web/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -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(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "80px",
background: "linear-gradient(135deg, #0B1220 0%, #10203A 100%)",
color: "white",
fontFamily: "sans-serif",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "22px",
marginBottom: "36px",
}}
>
<div
style={{
width: "80px",
height: "80px",
borderRadius: "20px",
background: "#377FF6",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "48px",
fontWeight: 700,
}}
>
E
</div>
<div style={{ fontSize: "52px", fontWeight: 700 }}>Echo</div>
</div>

<div
style={{
display: "flex",
fontSize: "66px",
fontWeight: 800,
lineHeight: 1.1,
maxWidth: "980px",
}}
>
AI-Powered Customer Support Platform
</div>

<div
style={{
display: "flex",
fontSize: "30px",
color: "#9DB2CE",
marginTop: "28px",
maxWidth: "1000px",
lineHeight: 1.35,
}}
>
Embeddable chat &amp; voice widget · RAG-grounded AI agent · real-time
human takeover — installed with one line of code.
</div>

<div style={{ display: "flex", gap: "16px", marginTop: "48px" }}>
{["Next.js", "Convex", "Gemini", "Vapi", "Clerk"].map((tech) => (
<div
key={tech}
style={{
display: "flex",
fontSize: "24px",
padding: "10px 24px",
border: "1px solid #2A3B57",
borderRadius: "999px",
color: "#C7D6EC",
}}
>
{tech}
</div>
))}
</div>
</div>,
{ ...size }
)
}
3 changes: 3 additions & 0 deletions apps/web/app/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 9 additions & 1 deletion apps/web/proxy.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down
Loading