mirror of https://github.com/kortix-ai/suna.git
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:
commit
cae19692e1
Binary file not shown.
After Width: | Height: | Size: 259 KiB |
|
@ -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',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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}</>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue