Merge pull request #270 from rishimohan/conversation-share-page-seo-and-og-improvements

feat: add SSR SEO meta and dynamic OG public agent replay pages
This commit is contained in:
Marko Kraemer 2025-05-13 23:37:12 +02:00 committed by GitHub
commit cae19692e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 197 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

View File

@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get('title');
const response = await fetch(`https://api.orshot.com/v1/studio/render`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.ORSHOT_API_KEY}`,
},
body: JSON.stringify({
templateId: 10,
modifications: {
title,
},
response: {
type: 'binary',
},
}),
});
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const image = Buffer.from(arrayBuffer);
return new NextResponse(image, {
status: 200,
headers: {
'Content-Type': 'image/png',
},
});
}

View File

@ -1,19 +1,59 @@
import { Metadata } from 'next';
import { getThread, getProject } from '@/lib/api-server';
export const metadata: Metadata = {
title: 'Shared Conversation',
description: 'View a shared AI conversation',
export async function generateMetadata({ params }): Promise<Metadata> {
const { threadId } = await params;
const fallbackMetaData = {
title: 'Shared Conversation | Kortix Suna',
description: 'Replay this Agent conversation on Kortix Suna',
openGraph: {
title: 'Shared AI Conversation',
description: 'View a shared AI conversation from Kortix Suna',
images: ['/kortix-logo.png'],
title: 'Shared Conversation | Kortix Suna',
description: 'Replay this Agent conversation on Kortix Suna',
images: [`${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`],
},
};
};
export default function ThreadLayout({
children,
}: {
children: React.ReactNode;
}) {
try {
const threadData = await getThread(threadId);
const projectData = await getProject(threadData.project_id);
if (!threadData || !projectData) {
return fallbackMetaData;
}
const isDevelopment =
process.env.NODE_ENV === 'development' ||
process.env.NEXT_PUBLIC_ENV_MODE === 'LOCAL' ||
process.env.NEXT_PUBLIC_ENV_MODE === 'local';
const title = projectData.name || 'Shared Conversation | Kortix Suna';
const description = projectData.description || 'Replay this Agent conversation on Kortix Suna';
const ogImage = isDevelopment
? `${process.env.NEXT_PUBLIC_URL}/share-page/og-fallback.png`
: `${process.env.NEXT_PUBLIC_URL}/api/share-page/og-image?title=${projectData.name}`;
return {
title,
description,
openGraph: {
title,
description,
images: [ogImage],
},
twitter: {
title,
description,
images: ogImage,
creator: '@kortixai',
card: 'summary_large_image',
},
};
} catch (error) {
return fallbackMetaData;
}
}
export default async function ThreadLayout({ children }) {
return <>{children}</>;
}

View File

@ -0,0 +1,109 @@
import { Project, Thread } from '@/lib/api';
import { createClient } from '@/lib/supabase/server';
export const getThread = async (threadId: string): Promise<Thread> => {
const supabase = await createClient();
const { data, error } = await supabase
.from('threads')
.select('*')
.eq('thread_id', threadId)
.single();
if (error) throw error;
return data;
};
export const getProject = async (projectId: string): Promise<Project> => {
const supabase = await createClient();
try {
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('project_id', projectId)
.single();
console.log('Raw project data from database:', data);
if (error) {
// Handle the specific "no rows returned" error from Supabase
if (error.code === 'PGRST116') {
throw new Error(`Project not found or not accessible: ${projectId}`);
}
throw error;
}
console.log('Raw project data from database:', data);
// // If project has a sandbox, ensure it's started
// if (data.sandbox?.id) {
// // Fire off sandbox activation without blocking
// const ensureSandboxActive = async () => {
// try {
// const {
// data: { session },
// } = await supabase.auth.getSession();
// // For public projects, we don't need authentication
// const headers: Record<string, string> = {
// 'Content-Type': 'application/json',
// };
// if (session?.access_token) {
// headers['Authorization'] = `Bearer ${session.access_token}`;
// }
// console.log(`Ensuring sandbox is active for project ${projectId}...`);
// const response = await fetch(
// `${API_URL}/project/${projectId}/sandbox/ensure-active`,
// {
// method: 'POST',
// headers,
// },
// );
// if (!response.ok) {
// const errorText = await response
// .text()
// .catch(() => 'No error details available');
// console.warn(
// `Failed to ensure sandbox is active: ${response.status} ${response.statusText}`,
// errorText,
// );
// } else {
// console.log('Sandbox activation successful');
// }
// } catch (sandboxError) {
// console.warn('Failed to ensure sandbox is active:', sandboxError);
// }
// };
// // Start the sandbox activation without awaiting
// ensureSandboxActive();
// }
// Map database fields to our Project type
const mappedProject: Project = {
id: data.project_id,
name: data.name || '',
description: data.description || '',
account_id: data.account_id,
is_public: data.is_public || false,
created_at: data.created_at,
sandbox: data.sandbox || {
id: '',
pass: '',
vnc_preview: '',
sandbox_url: '',
},
};
console.log('Mapped project data for frontend:', mappedProject);
return mappedProject;
} catch (error) {
console.error(`Error fetching project ${projectId}:`, error);
throw error;
}
};