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
5 changes: 4 additions & 1 deletion apps/docs/app/llms.mdx/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { source } from '@/lib/source'

export const revalidate = false

export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) {
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ slug?: string[] }> }
) {
const { slug } = await params

let lang: (typeof i18n.languages)[number] = i18n.defaultLanguage
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/copilot/chats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('CopilotChatsListAPI')

export async function GET(_req: NextRequest) {
export async function GET(_request: NextRequest) {
try {
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !userId) {
Expand Down
10 changes: 4 additions & 6 deletions apps/sim/app/api/files/serve/[...path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,13 @@ export async function GET(
const cloudKey = isCloudPath ? path.slice(1).join('/') : fullPath

const contextParam = request.nextUrl.searchParams.get('context')
const legacyBucketType = request.nextUrl.searchParams.get('bucket')

const context = contextParam || (isCloudPath ? inferContextFromKey(cloudKey) : undefined)

if (context === 'profile-pictures') {
logger.info('Serving public profile picture:', { cloudKey })
if (context === 'profile-pictures' || context === 'og-images') {
logger.info(`Serving public ${context}:`, { cloudKey })
if (isUsingCloudStorage() || isCloudPath) {
return await handleCloudProxyPublic(cloudKey, context, legacyBucketType)
return await handleCloudProxyPublic(cloudKey, context)
}
return await handleLocalFilePublic(fullPath)
}
Expand Down Expand Up @@ -182,8 +181,7 @@ async function handleCloudProxy(

async function handleCloudProxyPublic(
cloudKey: string,
context: StorageContext,
legacyBucketType?: string | null
context: StorageContext
): Promise<NextResponse> {
try {
let fileBuffer: Buffer
Expand Down
7 changes: 5 additions & 2 deletions apps/sim/app/api/knowledge/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const UpdateKnowledgeBaseSchema = z.object({
.optional(),
})

export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params

Expand Down Expand Up @@ -133,7 +133,10 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
}
}

export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = generateRequestId()
const { id } = await params

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const updateInvitationSchema = z.object({

// Get invitation details
export async function GET(
_req: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ id: string; invitationId: string }> }
) {
const { id: organizationId, invitationId } = await params
Expand Down
132 changes: 132 additions & 0 deletions apps/sim/app/api/templates/[id]/og-image/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { db } from '@sim/db'
import { templates } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import { uploadFile } from '@/lib/uploads/core/storage-service'
import { isValidPng } from '@/lib/uploads/utils/validation'

const logger = createLogger('TemplateOGImageAPI')

/**
* PUT /api/templates/[id]/og-image
* Upload a pre-generated OG image for a template.
* Accepts base64-encoded image data in the request body.
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params

try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized OG image upload attempt for template: ${id}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const [template] = await db
.select({ id: templates.id, workflowId: templates.workflowId })
.from(templates)
.where(eq(templates.id, id))
.limit(1)

if (!template) {
logger.warn(`[${requestId}] Template not found for OG image upload: ${id}`)
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
}

const body = await request.json()
const { imageData } = body

if (!imageData || typeof imageData !== 'string') {
return NextResponse.json(
{ error: 'Missing or invalid imageData (expected base64 string)' },
{ status: 400 }
)
}

const base64Data = imageData.includes(',') ? imageData.split(',')[1] : imageData
const imageBuffer = Buffer.from(base64Data, 'base64')

if (!isValidPng(imageBuffer)) {
return NextResponse.json({ error: 'Invalid PNG image data' }, { status: 400 })
}

const maxSize = 5 * 1024 * 1024
if (imageBuffer.length > maxSize) {
return NextResponse.json({ error: 'Image too large. Maximum size is 5MB.' }, { status: 400 })
}

const timestamp = Date.now()
const storageKey = `og-images/templates/${id}/${timestamp}.png`

logger.info(`[${requestId}] Uploading OG image for template ${id}: ${storageKey}`)

const uploadResult = await uploadFile({
file: imageBuffer,
fileName: storageKey,
contentType: 'image/png',
context: 'og-images',
preserveKey: true,
customKey: storageKey,
})

const baseUrl = getBaseUrl()
const ogImageUrl = `${baseUrl}${uploadResult.path}?context=og-images`

await db
.update(templates)
.set({
ogImageUrl,
updatedAt: new Date(),
})
.where(eq(templates.id, id))

logger.info(`[${requestId}] Successfully uploaded OG image for template ${id}: ${ogImageUrl}`)

return NextResponse.json({
success: true,
ogImageUrl,
})
} catch (error: unknown) {
logger.error(`[${requestId}] Error uploading OG image for template ${id}:`, error)
return NextResponse.json({ error: 'Failed to upload OG image' }, { status: 500 })
}
}

/**
* DELETE /api/templates/[id]/og-image
* Remove the OG image for a template.
*/
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = generateRequestId()
const { id } = await params

try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

await db
.update(templates)
.set({
ogImageUrl: null,
updatedAt: new Date(),
})
.where(eq(templates.id, id))

logger.info(`[${requestId}] Removed OG image for template ${id}`)

return NextResponse.json({ success: true })
} catch (error: unknown) {
logger.error(`[${requestId}] Error removing OG image for template ${id}:`, error)
return NextResponse.json({ error: 'Failed to remove OG image' }, { status: 500 })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export async function GET(

// DELETE /api/workspaces/invitations/[invitationId] - Delete a workspace invitation
export async function DELETE(
_req: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ invitationId: string }> }
) {
const { invitationId } = await params
Expand Down Expand Up @@ -221,7 +221,7 @@ export async function DELETE(

// POST /api/workspaces/invitations/[invitationId] - Resend a workspace invitation
export async function POST(
_req: NextRequest,
_request: NextRequest,
{ params }: { params: Promise<{ invitationId: string }> }
) {
const { invitationId } = await params
Expand Down
21 changes: 7 additions & 14 deletions apps/sim/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,24 @@ export const metadata: Metadata = {
locale: 'en_US',
images: [
{
url: '/social/og-image.png',
width: 1200,
height: 630,
alt: 'Sim - Visual AI Workflow Builder',
url: '/logo/primary/rounded.png',
width: 512,
height: 512,
alt: 'Sim - AI Agent Workflow Builder',
type: 'image/png',
},
{
url: '/social/og-image-square.png',
width: 600,
height: 600,
alt: 'Sim Logo',
},
],
},
twitter: {
card: 'summary_large_image',
card: 'summary',
site: '@simdotai',
creator: '@simdotai',
title: 'Sim - AI Agent Workflow Builder | Open Source',
description:
'Open-source platform for agentic workflows. 60,000+ developers. Visual builder. 100+ integrations. SOC2 & HIPAA compliant.',
images: {
url: '/social/twitter-image.png',
alt: 'Sim - Visual AI Workflow Builder',
url: '/logo/primary/rounded.png',
alt: 'Sim - AI Agent Workflow Builder',
},
},
alternates: {
Expand All @@ -77,7 +71,6 @@ export const metadata: Metadata = {
category: 'technology',
classification: 'AI Development Tools',
referrer: 'origin-when-cross-origin',
// LLM SEO optimizations
other: {
'llm:content-type': 'AI workflow builder, visual programming, no-code AI development',
'llm:use-cases':
Expand Down
83 changes: 83 additions & 0 deletions apps/sim/app/templates/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,88 @@
import { db } from '@sim/db'
import { templateCreators, templates } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { Metadata } from 'next'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import TemplateDetails from '@/app/templates/[id]/template'

const logger = createLogger('TemplateMetadata')

/**
* Generate dynamic metadata for template pages.
* This provides OpenGraph images for social media sharing.
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params

try {
const result = await db
.select({
template: templates,
creator: templateCreators,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.id, id))
.limit(1)

if (result.length === 0) {
return {
title: 'Template Not Found',
description: 'The requested template could not be found.',
}
}

const { template, creator } = result[0]
const baseUrl = getBaseUrl()

const details = template.details as { tagline?: string; about?: string } | null
const description = details?.tagline || 'AI workflow template on Sim'

const hasOgImage = !!template.ogImageUrl
const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`

return {
title: template.name,
description,
openGraph: {
title: template.name,
description,
type: 'website',
url: `${baseUrl}/templates/${id}`,
siteName: 'Sim',
images: [
{
url: ogImageUrl,
width: hasOgImage ? 1200 : 512,
height: hasOgImage ? 630 : 512,
alt: `${template.name} - Workflow Preview`,
},
],
},
twitter: {
card: hasOgImage ? 'summary_large_image' : 'summary',
title: template.name,
description,
images: [ogImageUrl],
creator: creator?.details
? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
: undefined,
},
}
} catch (error) {
logger.error('Failed to generate template metadata:', error)
return {
title: 'Template',
description: 'AI workflow template on Sim',
}
}
}

/**
* Public template detail page for unauthenticated users.
* Authenticated-user redirect is handled in templates/[id]/layout.tsx.
Expand Down
Loading
Loading