diff --git a/apps/docs/content/docs/api-reference/getting-started.mdx b/apps/docs/content/docs/api-reference/getting-started.mdx index 07f511940df..e080feb9125 100644 --- a/apps/docs/content/docs/api-reference/getting-started.mdx +++ b/apps/docs/content/docs/api-reference/getting-started.mdx @@ -14,6 +14,8 @@ Sim Cloud uses `https://www.sim.ai`, with API paths under `/api/v2`. There is no Download the [OpenAPI 3.1 specification](/openapi.json) for client generation, request validation, and API tooling. +Official SDKs wrap these calls: [TypeScript](/api-reference/typescript) (`bun add simstudio-ts-sdk`) and [Python](/api-reference/python) (`pip install simstudio-sdk`). The [CLI](/cli) (`bun add -g sim`) runs workflows and manages resources from a terminal. + ## Run a deployed workflow [#quick-start] diff --git a/apps/docs/lib/redirects.ts b/apps/docs/lib/redirects.ts index 1947af52426..884410cdc9f 100644 --- a/apps/docs/lib/redirects.ts +++ b/apps/docs/lib/redirects.ts @@ -98,6 +98,10 @@ const PATH_REDIRECTS: DocsRedirect[] = [ permanent: true, }, { source: '/api-reference', destination: '/api-reference/getting-started', permanent: true }, + // sdks/* folded into api-reference/ + { source: '/sdks', destination: '/api-reference/getting-started', permanent: true }, + { source: '/sdks/typescript', destination: '/api-reference/typescript', permanent: true }, + { source: '/sdks/python', destination: '/api-reference/python', permanent: true }, // building-agents/agents merged into the building-agents overview { source: '/building-agents/agents', destination: '/agents', permanent: true }, // form deployment removed diff --git a/apps/sim/app/(landing)/components/hero/hero.tsx b/apps/sim/app/(landing)/components/hero/hero.tsx index 74ea5744c5f..f3ca58f5086 100644 --- a/apps/sim/app/(landing)/components/hero/hero.tsx +++ b/apps/sim/app/(landing)/components/hero/hero.tsx @@ -1,8 +1,11 @@ -import { INTEGRATION_COUNT_LABEL } from '@sim/deployment-config/integration-claims' import { cn } from '@sim/emcn' import { HeroAnnouncementChip } from '@/app/(landing)/components/hero/components/hero-announcement-chip' import { LandingHeroHeader } from '@/app/(landing)/components/hero/components/hero-header' import { HeroPlatformStage } from '@/app/(landing)/components/hero/components/hero-platform-stage' +import { + HOME_HERO_HEADING, + HOME_HERO_SUMMARY, +} from '@/app/(landing)/components/home-structured-data' import { HOME_INSET, LANDING_CONTENT_WIDTH, @@ -30,12 +33,7 @@ export function Hero() { LANDING_GUTTER )} > -

- Sim is the open-source AI workspace where teams build, deploy, and manage AI agents for - their organization. Connect {INTEGRATION_COUNT_LABEL} integrations and every major LLM, then - govern access, spend, data, and deployment from one place. Build visually, conversationally, - or with code, and run Sim in your own cloud. -

+

{HOME_HERO_SUMMARY}

} heading={ <> - Build and govern + {HOME_HERO_HEADING[0]}
- every AI agent in one place + {HOME_HERO_HEADING[1]} } description='Build, deploy, and manage AI agents with central control over access, spend, and data.' diff --git a/apps/sim/app/(landing)/components/home-structured-data/home-structured-data.tsx b/apps/sim/app/(landing)/components/home-structured-data/home-structured-data.tsx index e1b776d3706..ee4e2de2644 100644 --- a/apps/sim/app/(landing)/components/home-structured-data/home-structured-data.tsx +++ b/apps/sim/app/(landing)/components/home-structured-data/home-structured-data.tsx @@ -32,6 +32,42 @@ export const HOME_PAGE_DESCRIPTION = `Sim is the open-source AI workspace where */ export const HOME_PAGE_TITLE = 'The AI Workspace | Build, Distribute & Govern AI Agents | Sim' +/** The hero's `

`, one entry per rendered line. */ +export const HOME_HERO_HEADING = ['Build and govern', 'every AI agent in one place'] as const + +/** + * The hero's summary paragraph, shared with the homepage's Markdown representation (`/index.md`). + */ +export const HOME_HERO_SUMMARY = `Sim is the open-source AI workspace where teams build, deploy, and manage AI agents for their organization. Connect ${INTEGRATION_COUNT_LABEL} integrations and every major LLM, then govern access, spend, data, and deployment from one place. Build visually, conversationally, or with code, and run Sim in your own cloud.` + +/** + * The product's feature claims, shared by the JSON-LD `featureList` below and the homepage's + * Markdown representation (`/index.md`). + */ +export const HOME_FEATURES = [ + 'AI workspace for teams', + 'Chat: build and manage agents in natural language', + 'Visual workflow builder', + 'CLI access for coding agents and terminal workflows', + `${INTEGRATION_COUNT_LABEL} integrations`, + 'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)', + 'Knowledge base creation', + 'Table creation', + 'Document creation', + 'API access', + 'Custom functions', + 'Scheduled workflows', + 'Event triggers', + 'Permission groups for model, integration, and tool access', + 'Organization and per-member spend limits', + 'Usage analytics by workspace and member', + 'Agent deployment as chat, API, or MCP with SSO, email, or password access', + 'Single sign-on with domain verification', + 'Audit records and run tracing', + 'Configurable data retention', + 'Self-hosting with Docker or Kubernetes', +] as const + const HOME_JSON_LD = { '@context': 'https://schema.org', '@graph': [ @@ -68,29 +104,7 @@ const HOME_JSON_LD = { browserRequirements: 'Requires a modern browser with JavaScript enabled', installUrl: `${SITE_URL}/signup`, publisher: { '@id': `${SITE_URL}#organization` }, - featureList: [ - 'AI workspace for teams', - 'Chat: build and manage agents in natural language', - 'Visual workflow builder', - 'CLI access for coding agents and terminal workflows', - `${INTEGRATION_COUNT_LABEL} integrations`, - 'LLM orchestration (OpenAI, Anthropic, Google, xAI, Mistral, Perplexity)', - 'Knowledge base creation', - 'Table creation', - 'Document creation', - 'API access', - 'Custom functions', - 'Scheduled workflows', - 'Event triggers', - 'Permission groups for model, integration, and tool access', - 'Organization and per-member spend limits', - 'Usage analytics by workspace and member', - 'Agent deployment as chat, API, or MCP with SSO, email, or password access', - 'Single sign-on with domain verification', - 'Audit records and run tracing', - 'Configurable data retention', - 'Self-hosting with Docker or Kubernetes', - ], + featureList: HOME_FEATURES, }, { '@type': 'SoftwareSourceCode', diff --git a/apps/sim/app/(landing)/components/home-structured-data/index.ts b/apps/sim/app/(landing)/components/home-structured-data/index.ts index d4a9af3c897..021cc81ac39 100644 --- a/apps/sim/app/(landing)/components/home-structured-data/index.ts +++ b/apps/sim/app/(landing)/components/home-structured-data/index.ts @@ -1 +1,8 @@ -export { HOME_PAGE_DESCRIPTION, HOME_PAGE_TITLE, HomeStructuredData } from './home-structured-data' +export { + HOME_FEATURES, + HOME_HERO_HEADING, + HOME_HERO_SUMMARY, + HOME_PAGE_DESCRIPTION, + HOME_PAGE_TITLE, + HomeStructuredData, +} from './home-structured-data' diff --git a/apps/sim/app/(landing)/page.tsx b/apps/sim/app/(landing)/page.tsx index b6ac4c57574..3e418c53a51 100644 --- a/apps/sim/app/(landing)/page.tsx +++ b/apps/sim/app/(landing)/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { SITE_URL } from '@/lib/core/utils/urls' +import { SITE_URL, toSiteUrl } from '@/lib/core/utils/urls' import { HOME_PAGE_DESCRIPTION, HOME_PAGE_TITLE, @@ -54,6 +54,7 @@ export const metadata: Metadata = { }, alternates: { canonical: SITE_URL, + types: { 'text/markdown': toSiteUrl('/index.md') }, languages: { 'en-US': SITE_URL, 'x-default': SITE_URL, diff --git a/apps/sim/app/index.md/route.ts b/apps/sim/app/index.md/route.ts new file mode 100644 index 00000000000..aa23f7cd198 --- /dev/null +++ b/apps/sim/app/index.md/route.ts @@ -0,0 +1,79 @@ +import { SITE_URL } from '@/lib/core/utils/urls' +import { DOCS_URL } from '@/lib/help-links' +import { + HOME_FEATURES, + HOME_HERO_HEADING, + HOME_HERO_SUMMARY, + HOME_PAGE_DESCRIPTION, +} from '@/app/(landing)/components/home-structured-data' +import { PLATFORM_MENU } from '@/app/(landing)/components/navbar/components/nav-menu-chip' +import { + DOCS_LINE, + GITHUB_LINE, + linkLine, + markdownResponse, + navMenuLines, + PRICING_LINE, + SITEMAP_LINE, + SOLUTION_LINES, + section, +} from '@/app/llms.txt/llms' + +export const dynamic = 'force-static' +export const revalidate = 3600 + +/** + * The homepage as Markdown. `next.config.ts` rewrites `/` here when `Accept` asks for + * `text/markdown`, so this body is also served at `/`. It is `no-store` so a cache that ignores + * `Vary` can never hand it to a browser; at worst such a cache serves the HTML to a Markdown + * client. + */ +export async function GET() { + const response = markdownResponse( + [ + `# Sim: ${HOME_HERO_HEADING.join(' ')}`, + `> ${HOME_PAGE_DESCRIPTION}`, + HOME_HERO_SUMMARY, + section('Get started', [ + linkLine('Sign up', '/signup', 'Create a free account'), + PRICING_LINE, + DOCS_LINE, + GITHUB_LINE, + ]), + section('Build with code', [ + linkLine( + 'API quickstart', + `${DOCS_URL}/api-reference/getting-started`, + 'Create an API key and run a deployed workflow' + ), + linkLine( + 'TypeScript SDK', + `${DOCS_URL}/api-reference/typescript`, + '`bun add simstudio-ts-sdk`' + ), + linkLine('Python SDK', `${DOCS_URL}/api-reference/python`, '`pip install simstudio-sdk`'), + linkLine('CLI', `${DOCS_URL}/cli`, '`bun add -g sim`'), + linkLine('MCP server', `${DOCS_URL}/mcp/overview`, 'Connect an AI app to Sim'), + ]), + section( + 'What teams do in Sim', + HOME_FEATURES.map((feature) => `- ${feature}`) + ), + section('Platform', navMenuLines(PLATFORM_MENU)), + section('Solutions', SOLUTION_LINES), + section('More', [ + linkLine('llms.txt', '/llms.txt', 'An index of every public page'), + linkLine( + 'llms-full.txt', + '/llms-full.txt', + 'Full text of the library, customer stories, and comparisons' + ), + SITEMAP_LINE, + ]), + ], + revalidate + ) + response.headers.set('Cache-Control', 'no-store') + response.headers.set('Link', `<${SITE_URL}>; rel="canonical"`) + return response +} diff --git a/apps/sim/app/llms.txt/llms.ts b/apps/sim/app/llms.txt/llms.ts index 78bb9df5bcb..ddd8aad98f4 100644 --- a/apps/sim/app/llms.txt/llms.ts +++ b/apps/sim/app/llms.txt/llms.ts @@ -1,5 +1,6 @@ import { INTEGRATION_REACH } from '@sim/deployment-config/integration-claims' import { SITE_URL, toSiteUrl } from '@/lib/core/utils/urls' +import { DOCS_URL } from '@/lib/help-links' import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip' import { COMPLIANCE_PAGE_DESCRIPTION } from '@/app/(landing)/solutions/compliance/compliance' import { ENGINEERING_PAGE_DESCRIPTION } from '@/app/(landing)/solutions/engineering/engineering' @@ -36,6 +37,20 @@ const SOLUTIONS = [ { title: 'AI agents for sales', path: '/solutions/sales', description: SALES_PAGE_DESCRIPTION }, ] as const +/** Link lines both `/llms.txt` and the homepage's Markdown (`/index.md`) list. */ +export const PRICING_LINE = linkLine('Pricing', '/pricing', 'Free, Pro, Max, and Enterprise plans') +export const DOCS_LINE = linkLine('Documentation', DOCS_URL, 'Guides, SDKs, and API reference') +export const GITHUB_LINE = linkLine( + 'GitHub', + 'https://github.com/simstudioai/sim', + 'Open-source codebase' +) +export const SITEMAP_LINE = linkLine( + 'Sitemap', + '/sitemap.xml', + 'Every public URL, including each integration and model' +) + /** One llms.txt entry per `/solutions/*` page. */ export const SOLUTION_LINES = SOLUTIONS.map((s) => linkLine(s.title, s.path, s.description)) diff --git a/apps/sim/app/llms.txt/route.ts b/apps/sim/app/llms.txt/route.ts index 56676417fcb..9a7e0f4f5ae 100644 --- a/apps/sim/app/llms.txt/route.ts +++ b/apps/sim/app/llms.txt/route.ts @@ -12,10 +12,14 @@ import { ALL_COMPETITORS } from '@/app/(landing)/comparisons/utils' import { PLATFORM_MENU } from '@/app/(landing)/components/navbar/components/nav-menu-chip' import { MODEL_PROVIDERS_WITH_CATALOGS } from '@/app/(landing)/models/utils' import { + DOCS_LINE, + GITHUB_LINE, LLMS_HEADER, linkLine, markdownResponse, navMenuLines, + PRICING_LINE, + SITEMAP_LINE, SOLUTION_LINES, section, } from '@/app/llms.txt/llms' @@ -44,7 +48,7 @@ export async function GET() { section('Platform', [ linkLine('Home', '/', 'Product overview and primary entry point'), ...navMenuLines(PLATFORM_MENU), - linkLine('Pricing', '/pricing', 'Free, Pro, Max, and Enterprise plans'), + PRICING_LINE, ]), section('Solutions', SOLUTION_LINES), section( @@ -98,7 +102,7 @@ export async function GET() { `${DOCS_URL}/llms-full.txt`, 'Full text of the Sim documentation' ), - linkLine('Documentation', DOCS_URL, 'Guides, SDKs, and API reference'), + DOCS_LINE, ]), section('Blog', [ linkLine('Blog', '/blog', 'Announcements, engineering deep dives, and product context'), @@ -106,16 +110,12 @@ export async function GET() { ]), section('Optional', [ linkLine('Changelog', '/changelog', 'Product updates and release notes'), - linkLine('GitHub', 'https://github.com/simstudioai/sim', 'Open-source codebase'), + GITHUB_LINE, linkLine('Community Slack', SLACK_COMMUNITY_URL, 'Community workspace'), linkLine('Terms of Service', '/terms'), linkLine('Privacy Policy', '/privacy'), linkLine('Cookie Policy', '/cookie-policy'), - linkLine( - 'Sitemap', - '/sitemap.xml', - 'Every public URL, including each integration and model' - ), + SITEMAP_LINE, ]), ], revalidate diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index e8914a48367..87cda652d68 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -12,6 +12,19 @@ import { import { LANDING_ROUTES } from './lib/landing/routes' import { LIBRARY_MERGED_SLUGS, LIBRARY_MOVED_BLOG_SLUGS } from './lib/library/retired-slugs' +/** + * `Accept` header patterns for Markdown negotiation. `text/markdown` must be one of the listed + * media ranges, and `q=0` (RFC 9110 §12.4.2: not acceptable) keeps the HTML page. Next anchors + * both patterns to the whole header. + */ +const ACCEPTS_MARKDOWN = '(.*,)?\\s*text/markdown\\s*([;,].*)?' +const REFUSES_MARKDOWN = + '(.*,)?\\s*text/markdown(\\s*;[^,]*?)?\\s*;\\s*q\\s*=\\s*0(\\.0{0,3})?\\s*(,.*)?' +const MARKDOWN_REQUEST = { + has: [{ type: 'header' as const, key: 'accept', value: ACCEPTS_MARKDOWN }], + missing: [{ type: 'header' as const, key: 'accept', value: REFUSES_MARKDOWN }], +} + const nextConfig: NextConfig = { devIndicators: false, poweredByHeader: false, @@ -264,6 +277,11 @@ const nextConfig: NextConfig = { ], async headers() { return [ + { + /** `/` answers in HTML or Markdown by `Accept` (see the `beforeFiles` rewrite). */ + source: '/', + headers: [{ key: 'Vary', value: 'Accept' }], + }, { // `/public`-served assets keep their path across deploys (no content // hash), so a shorter TTL + revalidation window bounds how long a @@ -669,7 +687,8 @@ const nextConfig: NextConfig = { }, async rewrites() { return { - beforeFiles: [], + /** `beforeFiles` so a Markdown client's `/` reaches `/index.md` ahead of the HTML page. */ + beforeFiles: [{ source: '/', ...MARKDOWN_REQUEST, destination: '/index.md' }], afterFiles: [ { source: '/favicon.ico', @@ -683,21 +702,11 @@ const nextConfig: NextConfig = { /** * Fallback rewrites run only after every page and dynamic route has failed to match, so * this answers a Markdown client's 404 in Markdown without shadowing any real route. - * `text/markdown` must be one of the listed media ranges, and `q=0` (RFC 9110 §12.4.2: - * not acceptable) keeps the HTML page. Next anchors both patterns to the whole header. */ fallback: [ { source: '/:path*', - has: [{ type: 'header', key: 'accept', value: '(.*,)?\\s*text/markdown\\s*([;,].*)?' }], - missing: [ - { - type: 'header', - key: 'accept', - value: - '(.*,)?\\s*text/markdown(\\s*;[^,]*?)?\\s*;\\s*q\\s*=\\s*0(\\.0{0,3})?\\s*(,.*)?', - }, - ], + ...MARKDOWN_REQUEST, destination: '/markdown-not-found', }, ], diff --git a/apps/sim/scripts/test-landing-seo-e2e.ts b/apps/sim/scripts/test-landing-seo-e2e.ts index bfd0f54b681..06c1ce93bca 100644 --- a/apps/sim/scripts/test-landing-seo-e2e.ts +++ b/apps/sim/scripts/test-landing-seo-e2e.ts @@ -50,7 +50,7 @@ const HOME_PATH = '/?home' const checks: { name: string; status: 'passed' | 'failed'; durationMs: number; error?: string }[] = [] -const responses: { path: string; status: number; location?: string }[] = [] +const responses: { path: string; accept?: string; status: number; location?: string }[] = [] const startedAt = new Date().toISOString() async function check(name: string, run: () => Promise) { @@ -71,17 +71,26 @@ async function check(name: string, run: () => Promise) { } async function get( - path: string -): Promise<{ status: number; location: string | null; body: string }> { + path: string, + accept?: string +): Promise<{ status: number; location: string | null; body: string; headers: Headers }> { const response = await fetch(new URL(path, baseUrl), { redirect: 'manual', - headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SimLandingSeoE2E/1.0)' }, + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; SimLandingSeoE2E/1.0)', + ...(accept ? { Accept: accept } : {}), + }, signal: AbortSignal.timeout(30_000), }) const body = await response.text() const location = response.headers.get('location') - responses.push({ path, status: response.status, ...(location ? { location } : {}) }) - return { status: response.status, location, body } + responses.push({ + path, + ...(accept ? { accept } : {}), + status: response.status, + ...(location ? { location } : {}), + }) + return { status: response.status, location, body, headers: response.headers } } function robotsDirectives(html: string): string[] { @@ -333,6 +342,31 @@ async function run() { } assert.deepEqual(missing, []) }) + + await check('the homepage answers a Markdown request in Markdown', async () => { + const markdown = await get(HOME_PATH, 'text/markdown') + assert.equal(markdown.status, 200) + assert.match(markdown.headers.get('content-type') ?? '', /^text\/markdown/) + assert.match(markdown.headers.get('vary') ?? '', /\bAccept\b/) + assert.equal(markdown.headers.get('cache-control'), 'no-store') + assert.match(markdown.body, /^# Sim: /) + assert.match(markdown.body, /\]\([^)]*\/signup\)/) + + const refused = await get(HOME_PATH, 'text/html, text/markdown;q=0') + assert.match(refused.headers.get('content-type') ?? '', /^text\/html/) + + const html = await get(HOME_PATH, 'text/html,application/xhtml+xml,*/*;q=0.8') + assert.match(html.headers.get('content-type') ?? '', /^text\/html/) + assert.match(html.headers.get('vary') ?? '', /\bAccept\b/) + const alternate = /