improvement(cdn): add cdn for large video assets with fallback to static assets (#809)

* added CDN for large assets with fallback to static assets

* remove video assets from docs

---------

Co-authored-by: waleedlatif <waleedlatif@waleedlatifs-MacBook-Pro.local>
This commit is contained in:
Waleed Latif
2025-07-28 12:15:41 -07:00
committed by GitHub
co-authored by waleedlatif
parent abed816afd
commit 510ce4b7da
34 changed files with 71 additions and 35 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="apps/sim/public/static/sim.png" alt="Sim Studio Logo" width="500"/>
<img src="https://nwkhgj772h6t23m2.public.blob.vercel-storage.com/static/sim.png" alt="Sim Studio Logo" width="500"/>
</p>
<p align="center">
@@ -15,7 +15,7 @@
</p>
<p align="center">
<img src="apps/sim/public/static/demo.gif" alt="Sim Studio Demo" width="800"/>
<img src="https://nwkhgj772h6t23m2.public.blob.vercel-storage.com/static/demo.gif" alt="Sim Studio Demo" width="800"/>
</p>
## Getting Started
+17 -7
View File
@@ -9,13 +9,23 @@ export function cn(...inputs: ClassValue[]) {
}
/**
* Get the full URL for a video asset stored in Vercel Blob
* Get the full URL for an asset stored in Vercel Blob or local fallback
* - If CDN is configured (NEXT_PUBLIC_BLOB_BASE_URL), uses CDN URL
* - Otherwise falls back to local static assets served from root path
*/
export function getAssetUrl(filename: string) {
const cdnBaseUrl = process.env.NEXT_PUBLIC_BLOB_BASE_URL
if (cdnBaseUrl) {
return `${cdnBaseUrl}/${filename}`
}
return `/${filename}`
}
/**
* Get the full URL for a video asset stored in Vercel Blob or local fallback
* - If CDN is configured (NEXT_PUBLIC_BLOB_BASE_URL), uses CDN URL
* - Otherwise falls back to local static assets served from root path
*/
export function getVideoUrl(filename: string) {
const baseUrl = process.env.NEXT_PUBLIC_BLOB_BASE_URL
if (!baseUrl) {
console.warn('NEXT_PUBLIC_BLOB_BASE_URL not configured, falling back to local path')
return `/${filename}`
}
return `${baseUrl}/${filename}`
return getAssetUrl(filename)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,7 @@
'use client'
import { motion } from 'framer-motion'
import { getAssetUrl } from '@/lib/utils'
import { BlogCard } from '@/app/(landing)/components/blog-card'
function Blogs() {
@@ -50,7 +51,7 @@ function Blogs() {
date={new Date('25 April 2025')}
author='Emir Ayaz'
authorRole='Designer'
avatar='/static/sim.png'
avatar={getAssetUrl('static/sim.png')}
type='Agents'
readTime='6'
/>
@@ -61,7 +62,7 @@ function Blogs() {
date={new Date('25 April 2025')}
author='Emir Ayaz'
authorRole='Designer'
avatar='/static/sim.png'
avatar={getAssetUrl('static/sim.png')}
type='Agents'
readTime='6'
/>
@@ -80,10 +81,10 @@ function Blogs() {
date={new Date('25 April 2025')}
author='Emir Ayaz'
authorRole='Designer'
avatar='/static/sim.png'
avatar={getAssetUrl('static/sim.png')}
type='Agents'
readTime='6'
image='/static/hero.png'
image={getAssetUrl('static/hero.png')}
/>
<BlogCard
href='/blog/test'
@@ -91,7 +92,7 @@ function Blogs() {
description="Learn how to create a fully functional AI agent using SimStudio.ai's unified API and workflows."
author='Emir Ayaz'
authorRole='Designer'
avatar='/static/sim.png'
avatar={getAssetUrl('static/sim.png')}
type='Agents'
readTime='6'
/>
@@ -110,7 +111,7 @@ function Blogs() {
date={new Date('25 April 2025')}
author='Emir Ayaz'
authorRole='Designer'
avatar='/static/sim.png'
avatar={getAssetUrl('static/sim.png')}
type='Agents'
readTime='6'
/>
@@ -121,7 +122,7 @@ function Blogs() {
date={new Date('25 April 2025')}
author='Emir Ayaz'
authorRole='Designer'
avatar='/static/sim.png'
avatar={getAssetUrl('static/sim.png')}
type='Functions'
readTime='6'
/>
@@ -1,6 +1,7 @@
'use client'
import { motion } from 'framer-motion'
import { getAssetUrl } from '@/lib/utils'
import useIsMobile from '@/app/(landing)/components/hooks/use-is-mobile'
import { Marquee } from '@/app/(landing)/components/magicui/marquee'
@@ -10,63 +11,63 @@ const X_TESTIMONIALS = [
username: '@GithubProjects',
viewCount: '90.4k',
tweetUrl: 'https://x.com/GithubProjects/status/1906383555707490499',
profileImage: '/twitter/github-projects.jpg',
profileImage: getAssetUrl('twitter/github-projects.jpg'),
},
{
text: 'A very good looking agent workflow builder 🔥 and open source!',
username: '@xyflowdev',
viewCount: '3,246',
tweetUrl: 'https://x.com/xyflowdev/status/1909501499719438670',
profileImage: '/twitter/xyflow.jpg',
profileImage: getAssetUrl('twitter/xyflow.jpg'),
},
{
text: "🚨 BREAKING: This startup just dropped the fastest way to build AI agents.\n\nThis Figma-like canvas to build agents will blow your mind.\n\nHere's why this is the best tool for building AI agents:",
username: '@hasantoxr',
viewCount: '515k',
tweetUrl: 'https://x.com/hasantoxr/status/1912909502036525271',
profileImage: '/twitter/hasan.jpg',
profileImage: getAssetUrl('twitter/hasan.jpg'),
},
{
text: 'omfggggg this is the zapier of agent building\n\ni always believed that building agents and using ai should not be limited to technical people. i think this solves just that\n\nthe fact that this is also open source makes me so optimistic about the future of building with ai :)))\n\ncongrats @karabegemir & @typingwala !!!',
username: '@nizzyabi',
viewCount: '6,269',
tweetUrl: 'https://x.com/nizzyabi/status/1907864421227180368',
profileImage: '/twitter/nizzy.jpg',
profileImage: getAssetUrl('twitter/nizzy.jpg'),
},
{
text: "One of the best products I've seen in the space, and the hustle and grind I've seen from @karabegemir and @typingwala is insane. Sim Studio is positioned to build something game-changing, and there's no better team for the job.\n\nCongrats on the launch 🚀 🎊 great things ahead!",
username: '@firestorm776',
viewCount: '956',
tweetUrl: 'https://x.com/firestorm776/status/1907896097735061598',
profileImage: '/twitter/samarth.jpg',
profileImage: getAssetUrl('twitter/samarth.jpg'),
},
{
text: 'lfgg got access to @simstudioai via @zerodotemail 😎',
username: '@nizzyabi',
viewCount: '1,585',
tweetUrl: 'https://x.com/nizzyabi/status/1910482357821595944',
profileImage: '/twitter/nizzy.jpg',
profileImage: getAssetUrl('twitter/nizzy.jpg'),
},
{
text: 'Feels like we\'re finally getting a "Photoshop moment" for AI devs—visual, intuitive, and fast enough to keep up with ideas mid-flow.',
username: '@syamrajk',
viewCount: '2,643',
tweetUrl: 'https://x.com/syamrajk/status/1912911980110946491',
profileImage: '/twitter/syamrajk.jpg',
profileImage: getAssetUrl('twitter/syamrajk.jpg'),
},
{
text: "🚨 BREAKING: This startup just dropped the fastest way to build AI agents.\n\nThis Figma-like canvas to build agents will blow your mind.\n\nHere's why this is the best tool for building AI agents:",
username: '@lazukars',
viewCount: '47.4k',
tweetUrl: 'https://x.com/lazukars/status/1913136390503600575',
profileImage: '/twitter/lazukars.png',
profileImage: getAssetUrl('twitter/lazukars.png'),
},
{
text: 'The use cases are endless. Great work @simstudioai',
username: '@daniel_zkim',
viewCount: '103',
tweetUrl: 'https://x.com/daniel_zkim/status/1907891273664782708',
profileImage: '/twitter/daniel.jpg',
profileImage: getAssetUrl('twitter/daniel.jpg'),
},
]
+4 -3
View File
@@ -4,6 +4,7 @@ import type { Metadata, Viewport } from 'next'
import { PublicEnvScript } from 'next-runtime-env'
import { env, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getAssetUrl } from '@/lib/utils'
import { TelemetryConsentDialog } from '@/app/telemetry-consent-dialog'
import '@/app/globals.css'
@@ -118,7 +119,7 @@ export const metadata: Metadata = {
siteName: 'Sim Studio',
images: [
{
url: 'https://simstudio.ai/social/facebook.png',
url: getAssetUrl('social/facebook.png'),
width: 1200,
height: 630,
alt: 'Sim Studio',
@@ -130,7 +131,7 @@ export const metadata: Metadata = {
title: 'Sim Studio',
description:
'Build and deploy AI agents using our Figma-like canvas. Build, write evals, and deploy AI agent workflows that automate workflows and streamline your business processes.',
images: ['https://simstudio.ai/social/twitter.png'],
images: [getAssetUrl('social/twitter.png')],
creator: '@simstudioai',
site: '@simstudioai',
},
@@ -231,7 +232,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<meta name='twitter:domain' content='simstudio.ai' />
{/* Additional image sources */}
<link rel='image_src' href='https://simstudio.ai/social/facebook.png' />
<link rel='image_src' href={getAssetUrl('social/facebook.png')} />
<PublicEnvScript />
</head>
+4 -3
View File
@@ -1,5 +1,6 @@
import { Container, Img, Link, Section, Text } from '@react-email/components'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
interface UnsubscribeOptions {
unsubscribeToken?: string
@@ -25,13 +26,13 @@ export const EmailFooter = ({
<tr>
<td align='center' style={{ padding: '0 8px' }}>
<Link href='https://x.com/simstudioai' rel='noopener noreferrer'>
<Img src={`${baseUrl}/static/x-icon.png`} width='24' height='24' alt='X' />
<Img src={getAssetUrl('static/x-icon.png')} width='24' height='24' alt='X' />
</Link>
</td>
<td align='center' style={{ padding: '0 8px' }}>
<Link href='https://discord.gg/Hr4UWYEcTT' rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/discord-icon.png`}
src={getAssetUrl('static/discord-icon.png')}
width='24'
height='24'
alt='Discord'
@@ -41,7 +42,7 @@ export const EmailFooter = ({
<td align='center' style={{ padding: '0 8px' }}>
<Link href='https://github.com/simstudioai/sim' rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/github-icon.png`}
src={getAssetUrl('static/github-icon.png')}
width='24'
height='24'
alt='GitHub'
@@ -13,6 +13,7 @@ import {
} from '@react-email/components'
import { format } from 'date-fns'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -59,7 +60,7 @@ export const InvitationEmail = ({
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={`${baseUrl}/static/sim.png`}
src={getAssetUrl('static/sim.png')}
width='114'
alt='Sim Studio'
style={{
@@ -11,6 +11,7 @@ import {
Text,
} from '@react-email/components'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -68,7 +69,7 @@ export const OTPVerificationEmail = ({
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={`${baseUrl}/static/sim.png`}
src={getAssetUrl('static/sim.png')}
width='114'
alt='Sim Studio'
style={{
@@ -13,6 +13,7 @@ import {
} from '@react-email/components'
import { format } from 'date-fns'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -39,7 +40,7 @@ export const ResetPasswordEmail = ({
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={`${baseUrl}/static/sim.png`}
src={getAssetUrl('static/sim.png')}
width='114'
alt='Sim Studio'
style={{
@@ -12,6 +12,7 @@ import {
Text,
} from '@react-email/components'
import { env } from '@/lib/env'
import { getAssetUrl } from '@/lib/utils'
import { baseStyles } from './base-styles'
import EmailFooter from './footer'
@@ -56,7 +57,7 @@ export const WorkspaceInvitationEmail = ({
<Row>
<Column style={{ textAlign: 'center' }}>
<Img
src={`${baseUrl}/static/sim.png`}
src={getAssetUrl('static/sim.png')}
width='114'
alt='Sim Studio'
style={{
+4
View File
@@ -152,6 +152,9 @@ export const env = createEnv({
NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(), // Sentry DSN for client-side error tracking
NEXT_PUBLIC_SOCKET_URL: z.string().url().optional(), // WebSocket server URL for real-time features
// Asset Storage
NEXT_PUBLIC_BLOB_BASE_URL: z.string().url().optional(), // Base URL for Vercel Blob storage (CDN assets)
// Google Services - For client-side Google integrations
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().optional(), // Google OAuth client ID for browser auth
NEXT_PUBLIC_GOOGLE_API_KEY: z.string().optional(), // Google API key for client-side API calls
@@ -168,6 +171,7 @@ export const env = createEnv({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_BLOB_BASE_URL: process.env.NEXT_PUBLIC_BLOB_BASE_URL,
NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: process.env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
+1
View File
@@ -49,6 +49,7 @@ export const cspDirectives: CSPDirectives = {
'https://*.atlassian.com',
'https://cdn.discordapp.com',
'https://*.githubusercontent.com',
'https://*.public.blob.vercel-storage.com',
],
'media-src': ["'self'", 'blob:'],
+13
View File
@@ -379,6 +379,19 @@ export function getInvalidCharacters(name: string): string[] {
return invalidChars ? [...new Set(invalidChars)] : []
}
/**
* Get the full URL for an asset stored in Vercel Blob or local fallback
* - If CDN is configured (NEXT_PUBLIC_BLOB_BASE_URL), uses CDN URL
* - Otherwise falls back to local static assets served from root path
*/
export function getAssetUrl(filename: string) {
const cdnBaseUrl = env.NEXT_PUBLIC_BLOB_BASE_URL
if (cdnBaseUrl) {
return `${cdnBaseUrl}/${filename}`
}
return `/${filename}`
}
/**
* No-operation function for use as default callback
*/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 MiB