mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
1 Commits
v0.6.26
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75b1ed304e |
@@ -1,26 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, ChevronLeft } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export function BackLink() {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href='/blog'
|
||||
className='group flex items-center gap-1 text-[var(--landing-text-muted)] text-sm hover:text-[var(--landing-text)]'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
|
||||
>
|
||||
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
|
||||
{isHovered ? (
|
||||
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
|
||||
) : (
|
||||
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
</span>
|
||||
<svg
|
||||
className='h-3 w-3 shrink-0'
|
||||
viewBox='0 0 10 10'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<line
|
||||
x1='1'
|
||||
y1='5'
|
||||
x2='10'
|
||||
y2='5'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
|
||||
/>
|
||||
<path
|
||||
d='M6.5 2L3.5 5L6.5 8'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.33'
|
||||
strokeLinecap='square'
|
||||
strokeLinejoin='miter'
|
||||
fill='none'
|
||||
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
|
||||
/>
|
||||
</svg>
|
||||
Back to Blog
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -2,58 +2,51 @@ import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function BlogPostLoading() {
|
||||
return (
|
||||
<article className='w-full'>
|
||||
{/* Header area */}
|
||||
<div className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||
{/* Back link */}
|
||||
<article className='w-full bg-[var(--landing-bg)]'>
|
||||
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
|
||||
<div className='mb-6'>
|
||||
<Skeleton className='h-[16px] w-[60px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
{/* Image + title row */}
|
||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||
{/* Image */}
|
||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||
<Skeleton className='aspect-[450/360] w-full rounded-lg bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='aspect-[450/360] w-full rounded-[5px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
{/* Title + author */}
|
||||
<div className='flex flex-1 flex-col justify-between'>
|
||||
<div>
|
||||
<Skeleton className='h-[48px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-2 h-[48px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-2 h-[44px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-4 h-[18px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-2 h-[18px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
<div className='mt-4 flex items-center justify-between'>
|
||||
<div className='mt-6 flex items-center gap-6'>
|
||||
<Skeleton className='h-[12px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Skeleton className='h-[24px] w-[24px] rounded-full bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[20px] w-[20px] rounded-full bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
<Skeleton className='h-[32px] w-[32px] rounded-[6px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Divider */}
|
||||
<Skeleton className='mt-8 h-[1px] w-full bg-[var(--landing-bg-elevated)] sm:mt-12' />
|
||||
{/* Date + description */}
|
||||
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
|
||||
<Skeleton className='h-[16px] w-[120px] flex-shrink-0 rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<div className='flex-1 space-y-2'>
|
||||
<Skeleton className='h-[20px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[20px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
<div className='mx-auto max-w-[900px] px-6 py-16'>
|
||||
<div className='space-y-4'>
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Article body */}
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12'>
|
||||
<div className='space-y-4'>
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
|
||||
import { FAQ } from '@/lib/blog/faq'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { BackLink } from '@/app/(landing)/blog/[slug]/back-link'
|
||||
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
|
||||
@@ -30,27 +30,27 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
const { slug } = await params
|
||||
const post = await getPostBySlug(slug)
|
||||
const Article = post.Content
|
||||
const jsonLd = buildArticleJsonLd(post)
|
||||
const breadcrumbLd = buildBreadcrumbJsonLd(post)
|
||||
const graphJsonLd = buildPostGraphJsonLd(post)
|
||||
const related = await getRelatedPosts(slug, 3)
|
||||
|
||||
return (
|
||||
<article className='w-full' itemScope itemType='https://schema.org/BlogPosting'>
|
||||
<article
|
||||
className='w-full bg-[var(--landing-bg)]'
|
||||
itemScope
|
||||
itemType='https://schema.org/TechArticle'
|
||||
>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
|
||||
/>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
|
||||
/>
|
||||
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||
<header className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
|
||||
<div className='mb-6'>
|
||||
<BackLink />
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||
<div className='relative w-full overflow-hidden rounded-lg'>
|
||||
<div className='relative w-full overflow-hidden rounded-[5px]'>
|
||||
<Image
|
||||
src={post.ogImage}
|
||||
alt={post.title}
|
||||
@@ -65,18 +65,35 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-1 flex-col justify-between'>
|
||||
<h1
|
||||
className='text-balance font-[500] text-[36px] text-[var(--landing-text)] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
|
||||
itemProp='headline'
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
<div className='mt-4 flex items-center justify-between'>
|
||||
<div>
|
||||
<h1
|
||||
className='text-balance font-[430] font-season text-[28px] text-white leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
|
||||
itemProp='headline'
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
<p className='mt-4 font-[430] font-season text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'>
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-6 flex items-center gap-6'>
|
||||
<time
|
||||
className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'
|
||||
dateTime={post.date}
|
||||
itemProp='datePublished'
|
||||
>
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<meta itemProp='dateModified' content={post.updated ?? post.date} />
|
||||
<div className='flex items-center gap-3'>
|
||||
{(post.authors || [post.author]).map((a, idx) => (
|
||||
<div key={idx} className='flex items-center gap-2'>
|
||||
{a?.avatarUrl ? (
|
||||
<Avatar className='size-6'>
|
||||
<Avatar className='size-5'>
|
||||
<AvatarImage src={a.avatarUrl} alt={a.name} />
|
||||
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -85,7 +102,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
href={a?.url || '#'}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer author'
|
||||
className='text-[var(--landing-text-muted)] text-sm leading-[1.5] hover:text-[var(--landing-text)] sm:text-md'
|
||||
className='font-martian-mono text-[var(--landing-text-muted)] text-xs uppercase tracking-[0.1em] hover:text-white'
|
||||
itemProp='author'
|
||||
itemScope
|
||||
itemType='https://schema.org/Person'
|
||||
@@ -95,78 +112,72 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
|
||||
<div className='ml-auto'>
|
||||
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className='mt-8 border-[var(--landing-bg-elevated)] border-t sm:mt-12' />
|
||||
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
|
||||
<div className='flex flex-shrink-0 items-center gap-4'>
|
||||
<time
|
||||
className='block text-[var(--landing-text-muted)] text-sm leading-[1.5] sm:text-md'
|
||||
dateTime={post.date}
|
||||
itemProp='datePublished'
|
||||
>
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
<meta itemProp='dateModified' content={post.updated ?? post.date} />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='m-0 block translate-y-[-4px] font-[400] text-[var(--landing-text-muted)] text-lg leading-[1.5] sm:text-[20px] md:text-[26px]'>
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp='articleBody'>
|
||||
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-a:text-[var(--landing-text)] prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-[var(--landing-text)] prose-headings:text-[var(--landing-text)] prose-li:text-[var(--landing-text-muted)] prose-p:text-[var(--landing-text-muted)] prose-strong:text-[var(--landing-text)]'>
|
||||
<Article />
|
||||
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
{related.length > 0 && (
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
|
||||
<h2 className='mb-4 font-[500] text-[24px] text-[var(--landing-text)]'>Related posts</h2>
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
|
||||
{related.map((p) => (
|
||||
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
|
||||
<div className='overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)]'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
width={600}
|
||||
height={315}
|
||||
className='h-[160px] w-full object-cover'
|
||||
sizes='(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
loading='lazy'
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 text-[var(--landing-text-muted)] text-xs'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
<div className='font-[500] text-[var(--landing-text)] text-sm leading-tight'>
|
||||
{p.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
|
||||
<div className='mx-auto max-w-[900px] px-6 py-16' itemProp='articleBody'>
|
||||
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-headings:font-[430] prose-headings:font-season prose-a:text-white prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-white prose-headings:text-white prose-li:text-[var(--landing-text-body)] prose-p:text-[var(--landing-text-body)] prose-strong:text-white prose-headings:tracking-[-0.02em]'>
|
||||
<Article />
|
||||
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{related.length > 0 && (
|
||||
<>
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
<nav aria-label='Related posts' className='flex'>
|
||||
{related.map((p) => (
|
||||
<Link
|
||||
key={p.slug}
|
||||
href={`/blog/${p.slug}`}
|
||||
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
|
||||
>
|
||||
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
fill
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
className='object-cover'
|
||||
loading='lazy'
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
|
||||
{new Date(p.date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
year: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
<h3 className='font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em]'>
|
||||
{p.title}
|
||||
</h3>
|
||||
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
|
||||
{p.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
|
||||
<meta itemProp='publisher' content='Sim' />
|
||||
<meta itemProp='inLanguage' content='en-US' />
|
||||
<meta itemProp='keywords' content={post.tags.join(', ')} />
|
||||
{post.wordCount && <meta itemProp='wordCount' content={String(post.wordCount)} />}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,29 @@ export async function generateMetadata({
|
||||
const { id } = await params
|
||||
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
|
||||
const author = posts[0]?.author
|
||||
return { title: author?.name ?? 'Author' }
|
||||
const name = author?.name ?? 'Author'
|
||||
return {
|
||||
title: `${name} — Sim Blog`,
|
||||
description: `Read articles by ${name} on the Sim blog.`,
|
||||
alternates: { canonical: `https://sim.ai/blog/authors/${id}` },
|
||||
openGraph: {
|
||||
title: `${name} — Sim Blog`,
|
||||
description: `Read articles by ${name} on the Sim blog.`,
|
||||
url: `https://sim.ai/blog/authors/${id}`,
|
||||
siteName: 'Sim',
|
||||
type: 'profile',
|
||||
...(author?.avatarUrl
|
||||
? { images: [{ url: author.avatarUrl, width: 400, height: 400, alt: name }] }
|
||||
: {}),
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: `${name} — Sim Blog`,
|
||||
description: `Read articles by ${name} on the Sim blog.`,
|
||||
site: '@simdotai',
|
||||
...(author?.xHandle ? { creator: `@${author.xHandle}` } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -27,19 +49,41 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
</main>
|
||||
)
|
||||
}
|
||||
const personJsonLd = {
|
||||
const graphJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: author.name,
|
||||
url: `https://sim.ai/blog/authors/${author.id}`,
|
||||
sameAs: author.url ? [author.url] : [],
|
||||
image: author.avatarUrl,
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'Person',
|
||||
name: author.name,
|
||||
url: `https://sim.ai/blog/authors/${author.id}`,
|
||||
sameAs: author.url ? [author.url] : [],
|
||||
image: author.avatarUrl,
|
||||
worksFor: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://sim.ai/blog' },
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 3,
|
||||
name: author.name,
|
||||
item: `https://sim.ai/blog/authors/${author.id}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
|
||||
/>
|
||||
<div className='mb-6 flex items-center gap-3'>
|
||||
{author.avatarUrl ? (
|
||||
|
||||
@@ -9,8 +9,14 @@ export default async function StudioLayout({ children }: { children: React.React
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
description:
|
||||
'Sim is an open-source platform for building, testing, and deploying AI agent workflows.',
|
||||
logo: 'https://sim.ai/logo/primary/small.png',
|
||||
sameAs: ['https://x.com/simdotai'],
|
||||
sameAs: [
|
||||
'https://x.com/simdotai',
|
||||
'https://github.com/simstudioai/sim',
|
||||
'https://www.linkedin.com/company/simdotai',
|
||||
],
|
||||
}
|
||||
|
||||
const websiteJsonLd = {
|
||||
@@ -18,11 +24,6 @@ export default async function StudioLayout({ children }: { children: React.React
|
||||
'@type': 'WebSite',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: 'https://sim.ai/search?q={search_term_string}',
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,60 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { buildCollectionPageJsonLd } from '@/lib/blog/seo'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Blog',
|
||||
description: 'Announcements, insights, and guides from the Sim team.',
|
||||
export async function generateMetadata({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ page?: string; tag?: string }>
|
||||
}): Promise<Metadata> {
|
||||
const { page, tag } = await searchParams
|
||||
const pageNum = Math.max(1, Number(page || 1))
|
||||
|
||||
const titleParts = ['Blog']
|
||||
if (tag) titleParts.push(tag)
|
||||
if (pageNum > 1) titleParts.push(`Page ${pageNum}`)
|
||||
const title = titleParts.join(' — ')
|
||||
|
||||
const description = tag
|
||||
? `Sim blog posts tagged "${tag}" — insights and guides for building AI agent workflows.`
|
||||
: 'Announcements, insights, and guides for building AI agent workflows.'
|
||||
|
||||
const canonicalParams = new URLSearchParams()
|
||||
if (tag) canonicalParams.set('tag', tag)
|
||||
if (pageNum > 1) canonicalParams.set('page', String(pageNum))
|
||||
const qs = canonicalParams.toString()
|
||||
const canonical = `https://sim.ai/blog${qs ? `?${qs}` : ''}`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical },
|
||||
openGraph: {
|
||||
title: `${title} | Sim`,
|
||||
description,
|
||||
url: canonical,
|
||||
siteName: 'Sim',
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
images: [
|
||||
{
|
||||
url: 'https://sim.ai/logo/primary/medium.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'Sim Blog',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: `${title} | Sim`,
|
||||
description,
|
||||
site: '@simdotai',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const revalidate = 3600
|
||||
@@ -37,19 +86,13 @@ export default async function BlogIndex({
|
||||
const featured = pageNum === 1 ? posts.slice(0, 3) : []
|
||||
const remaining = pageNum === 1 ? posts.slice(3) : posts
|
||||
|
||||
const blogJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Blog',
|
||||
name: 'Sim Blog',
|
||||
url: 'https://sim.ai/blog',
|
||||
description: 'Announcements, insights, and guides for building AI agent workflows.',
|
||||
}
|
||||
const collectionJsonLd = buildCollectionPageJsonLd()
|
||||
|
||||
return (
|
||||
<section className='bg-[var(--landing-bg)]'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogJsonLd) }}
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionJsonLd) }}
|
||||
/>
|
||||
|
||||
{/* Section header */}
|
||||
@@ -81,7 +124,7 @@ export default async function BlogIndex({
|
||||
{/* Featured posts */}
|
||||
{featured.length > 0 && (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<nav aria-label='Featured posts' className='flex'>
|
||||
{featured.map((p, index) => (
|
||||
<Link
|
||||
key={p.slug}
|
||||
@@ -89,11 +132,14 @@ export default async function BlogIndex({
|
||||
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
|
||||
>
|
||||
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
|
||||
<img
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
className='h-full w-full object-cover'
|
||||
loading={index < 3 ? 'eager' : 'lazy'}
|
||||
fill
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
className='object-cover'
|
||||
priority={index < 3}
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
@@ -112,7 +158,7 @@ export default async function BlogIndex({
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
|
||||
</>
|
||||
@@ -151,12 +197,14 @@ export default async function BlogIndex({
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className='hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
|
||||
<img
|
||||
<div className='relative hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
|
||||
<Image
|
||||
src={p.ogImage}
|
||||
alt={p.title}
|
||||
className='h-full w-full object-cover'
|
||||
loading='lazy'
|
||||
fill
|
||||
sizes='140px'
|
||||
className='object-cover'
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -166,11 +214,12 @@ export default async function BlogIndex({
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className='px-6 py-8'>
|
||||
<nav aria-label='Pagination' className='px-6 py-8'>
|
||||
<div className='flex items-center justify-center gap-3'>
|
||||
{pageNum > 1 && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
rel='prev'
|
||||
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Previous
|
||||
@@ -182,13 +231,14 @@ export default async function BlogIndex({
|
||||
{pageNum < totalPages && (
|
||||
<Link
|
||||
href={`/blog?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
|
||||
rel='next'
|
||||
className='rounded-[5px] border border-[var(--landing-border-strong)] px-3 py-1 text-[var(--landing-text)] text-sm transition-colors hover:bg-[var(--landing-bg-elevated)]'
|
||||
>
|
||||
Next
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,13 +7,18 @@ export async function GET() {
|
||||
const posts = await getAllPostMeta()
|
||||
const items = posts.slice(0, 50)
|
||||
const site = 'https://sim.ai'
|
||||
const lastBuildDate =
|
||||
items.length > 0 ? new Date(items[0].date).toUTCString() : new Date().toUTCString()
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>Sim Blog</title>
|
||||
<link>${site}</link>
|
||||
<description>Announcements, insights, and guides for AI agent workflows.</description>
|
||||
<language>en-us</language>
|
||||
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
||||
<atom:link href="${site}/blog/rss.xml" rel="self" type="application/rss+xml" />
|
||||
${items
|
||||
.map(
|
||||
(p) => `
|
||||
@@ -26,6 +31,7 @@ export async function GET() {
|
||||
${(p.authors || [p.author])
|
||||
.map((a) => `<author><![CDATA[${a.name}${a.url ? ` (${a.url})` : ''}]]></author>`)
|
||||
.join('\n')}
|
||||
${p.tags.map((t) => `<category><![CDATA[${t}]]></category>`).join('\n ')}
|
||||
</item>`
|
||||
)
|
||||
.join('')}
|
||||
|
||||
@@ -4,12 +4,42 @@ import { getAllTags } from '@/lib/blog/registry'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tags',
|
||||
description: 'Browse Sim blog posts by topic — AI agents, workflows, integrations, and more.',
|
||||
alternates: { canonical: 'https://sim.ai/blog/tags' },
|
||||
openGraph: {
|
||||
title: 'Blog Tags | Sim',
|
||||
description: 'Browse Sim blog posts by topic — AI agents, workflows, integrations, and more.',
|
||||
url: 'https://sim.ai/blog/tags',
|
||||
siteName: 'Sim',
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: 'Blog Tags | Sim',
|
||||
description: 'Browse Sim blog posts by topic — AI agents, workflows, integrations, and more.',
|
||||
site: '@simdotai',
|
||||
},
|
||||
}
|
||||
|
||||
const breadcrumbJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://sim.ai/blog' },
|
||||
{ '@type': 'ListItem', position: 3, name: 'Tags', item: 'https://sim.ai/blog/tags' },
|
||||
],
|
||||
}
|
||||
|
||||
export default async function TagsIndex() {
|
||||
const tags = await getAllTags()
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<script
|
||||
type='application/ld+json'
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
<h1 className='mb-6 font-[500] text-[32px] text-[var(--landing-text)] leading-tight'>
|
||||
Browse by tag
|
||||
</h1>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
|
||||
|
||||
const logger = createLogger('github-stars')
|
||||
|
||||
const INITIAL_STARS = '27k'
|
||||
const INITIAL_STARS = '27.6k'
|
||||
|
||||
/**
|
||||
* Client component that displays GitHub stars count.
|
||||
|
||||
@@ -45,6 +45,8 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
{
|
||||
url: `${baseUrl}/blog`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/blog/tags`,
|
||||
@@ -72,6 +74,8 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const blogPages: MetadataRoute.Sitemap = posts.map((p) => ({
|
||||
url: p.canonical,
|
||||
lastModified: new Date(p.updated ?? p.date),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.7,
|
||||
}))
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
export function FAQ({ items }: { items: { q: string; a: string }[] }) {
|
||||
if (!items || items.length === 0) return null
|
||||
return (
|
||||
<section className='mt-12'>
|
||||
<section className='mt-12' itemScope itemType='https://schema.org/FAQPage'>
|
||||
<h2 className='mb-4 font-medium text-[24px] text-[var(--landing-text)]'>FAQ</h2>
|
||||
<div className='space-y-6'>
|
||||
{items.map((it, i) => (
|
||||
<div key={i}>
|
||||
<h3 className='mb-2 font-medium text-[20px] text-[var(--landing-text)]'>{it.q}</h3>
|
||||
<p className='text-[19px] text-[var(--text-subtle)] leading-relaxed'>{it.a}</p>
|
||||
<div key={i} itemScope itemType='https://schema.org/Question' itemProp='mainEntity'>
|
||||
<h3 className='mb-2 font-medium text-[20px] text-[var(--landing-text)]' itemProp='name'>
|
||||
{it.q}
|
||||
</h3>
|
||||
<div itemScope itemType='https://schema.org/Answer' itemProp='acceptedAnswer'>
|
||||
<p className='text-[19px] text-[var(--text-subtle)] leading-relaxed' itemProp='text'>
|
||||
{it.a}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -59,8 +59,15 @@ async function scanFrontmatters(): Promise<BlogMeta[]> {
|
||||
.catch(() => false)
|
||||
if (!hasMdx) continue
|
||||
const raw = await fs.readFile(mdxPath, 'utf-8')
|
||||
const { data } = matter(raw)
|
||||
const { data, content: mdxContent } = matter(raw)
|
||||
const fm = BlogFrontmatterSchema.parse(data)
|
||||
const wordCount = mdxContent
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
.replace(/import\s+.*?from\s+['"].*?['"]/g, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/[#*_~`[\]()!|>-]/g, '')
|
||||
.split(/\s+/)
|
||||
.filter((w) => w.length > 0).length
|
||||
const authors = fm.authors.map((id) => authorsMap[id]).filter(Boolean)
|
||||
if (authors.length === 0) throw new Error(`Authors not found for "${slug}"`)
|
||||
results.push({
|
||||
@@ -79,6 +86,7 @@ async function scanFrontmatters(): Promise<BlogMeta[]> {
|
||||
about: fm.about,
|
||||
timeRequired: fm.timeRequired,
|
||||
faq: fm.faq,
|
||||
wordCount,
|
||||
draft: fm.draft,
|
||||
featured: fm.featured ?? false,
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ export const BlogFrontmatterSchema = z
|
||||
)
|
||||
.optional(),
|
||||
canonical: z.string().url(),
|
||||
wordCount: z.number().int().positive().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
featured: z.boolean().default(false),
|
||||
})
|
||||
@@ -57,6 +58,7 @@ export interface BlogMeta {
|
||||
about?: string[]
|
||||
timeRequired?: string
|
||||
faq?: { q: string; a: string }[]
|
||||
wordCount?: number
|
||||
canonical: string
|
||||
draft: boolean
|
||||
featured: boolean
|
||||
|
||||
@@ -59,27 +59,33 @@ export function buildPostMetadata(post: BlogMeta): Metadata {
|
||||
|
||||
export function buildArticleJsonLd(post: BlogMeta) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
'@type': 'TechArticle',
|
||||
url: post.canonical,
|
||||
headline: post.title,
|
||||
description: post.description,
|
||||
image: [
|
||||
{
|
||||
'@type': 'ImageObject',
|
||||
url: post.ogImage,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
caption: post.ogAlt || post.title,
|
||||
},
|
||||
],
|
||||
datePublished: post.date,
|
||||
dateModified: post.updated ?? post.date,
|
||||
wordCount: post.wordCount,
|
||||
proficiencyLevel: 'Beginner',
|
||||
author: (post.authors && post.authors.length > 0 ? post.authors : [post.author]).map((a) => ({
|
||||
'@type': 'Person',
|
||||
name: a.name,
|
||||
url: a.url,
|
||||
...(a.url ? { sameAs: [a.url] } : {}),
|
||||
})),
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://sim.ai/logo/primary/medium.png',
|
||||
@@ -104,7 +110,6 @@ export function buildArticleJsonLd(post: BlogMeta) {
|
||||
|
||||
export function buildBreadcrumbJsonLd(post: BlogMeta) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
|
||||
@@ -117,7 +122,6 @@ export function buildBreadcrumbJsonLd(post: BlogMeta) {
|
||||
export function buildFaqJsonLd(items: { q: string; a: string }[] | undefined) {
|
||||
if (!items || items.length === 0) return null
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: items.map((it) => ({
|
||||
'@type': 'Question',
|
||||
@@ -127,6 +131,45 @@ export function buildFaqJsonLd(items: { q: string; a: string }[] | undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPostGraphJsonLd(post: BlogMeta) {
|
||||
const graph: Record<string, unknown>[] = [buildArticleJsonLd(post), buildBreadcrumbJsonLd(post)]
|
||||
|
||||
const faq = buildFaqJsonLd(post.faq)
|
||||
if (faq) {
|
||||
graph.push(faq)
|
||||
}
|
||||
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': graph,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildCollectionPageJsonLd() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
name: 'Sim Blog',
|
||||
url: 'https://sim.ai/blog',
|
||||
description: 'Announcements, insights, and guides for building AI agent workflows.',
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://sim.ai/logo/primary/medium.png',
|
||||
},
|
||||
},
|
||||
inLanguage: 'en-US',
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
name: 'Sim',
|
||||
url: 'https://sim.ai',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBlogJsonLd() {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
|
||||
38
apps/sim/public/llms.txt
Normal file
38
apps/sim/public/llms.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
# Sim
|
||||
|
||||
Sim is an open-source platform for building, testing, and deploying AI agent workflows visually. Create powerful AI agents, automation pipelines, and data processing workflows by connecting blocks on a canvas.
|
||||
|
||||
## Key Facts
|
||||
|
||||
- Website: https://sim.ai
|
||||
- GitHub: https://github.com/simstudioai/sim
|
||||
- Documentation: https://docs.sim.ai
|
||||
- License: Apache 2.0
|
||||
- Category: AI workflow automation, developer tools
|
||||
|
||||
## What Sim Does
|
||||
|
||||
- Visual workflow builder with drag-and-drop interface for AI agents
|
||||
- 80+ built-in integrations (OpenAI, Anthropic, Slack, Gmail, GitHub, and more)
|
||||
- Real-time team collaboration on workflows
|
||||
- Multiple deployment options: cloud-hosted or self-hosted
|
||||
- Custom integrations via MCP (Model Context Protocol)
|
||||
- API and SDK access (TypeScript, Python)
|
||||
- Workflow execution engine with logging and debugging
|
||||
|
||||
## Blog
|
||||
|
||||
The Sim blog covers announcements, technical deep-dives, and guides for building AI agent workflows.
|
||||
|
||||
- Blog: https://sim.ai/blog
|
||||
- RSS: https://sim.ai/blog/rss.xml
|
||||
|
||||
## Documentation Sections
|
||||
|
||||
- Getting Started: https://docs.sim.ai/getting-started
|
||||
- Blocks: https://docs.sim.ai/blocks
|
||||
- Tools & Integrations: https://docs.sim.ai/tools
|
||||
- Webhooks: https://docs.sim.ai/webhooks
|
||||
- MCP Protocol: https://docs.sim.ai/mcp
|
||||
- Deployment: https://docs.sim.ai/deployment
|
||||
- SDKs: https://docs.sim.ai/sdks
|
||||
Reference in New Issue
Block a user