diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml new file mode 100644 index 0000000000..b6966dc143 --- /dev/null +++ b/.github/workflows/i18n.yml @@ -0,0 +1,126 @@ +name: 'Auto-translate Documentation' + +on: + push: + branches: [ main ] + paths: + - 'apps/docs/content/docs/en/**' + - 'apps/docs/i18n.json' + pull_request: + branches: [ main ] + paths: + - 'apps/docs/content/docs/en/**' + - 'apps/docs/i18n.json' + workflow_dispatch: # Allow manual triggers + +jobs: + translate: + runs-on: ubuntu-latest + if: github.actor != 'github-actions[bot]' # Prevent infinite loops + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Run Lingo.dev translations + env: + LINGODOTDEV_API_KEY: ${{ secrets.LINGODOTDEV_API_KEY }} + run: | + cd apps/docs + bunx lingo.dev@latest i18n + + - name: Check for translation changes + id: changes + run: | + cd apps/docs + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + if [ -n "$(git status --porcelain content/docs)" ]; then + echo "changes=true" >> $GITHUB_OUTPUT + else + echo "changes=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push translation updates + if: steps.changes.outputs.changes == 'true' + run: | + cd apps/docs + git add content/docs/es/ content/docs/fr/ content/docs/zh/ i18n.lock + git commit -m "feat: update translations" + git push origin ${{ github.ref_name }} + + - name: Create Pull Request (for feature branches) + if: steps.changes.outputs.changes == 'true' && github.event_name == 'pull_request' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "feat: update translations" + title: "🌐 Auto-update translations" + body: | + ## Summary + Automated translation updates for documentation. + + - Updated translations for modified English content + - Generated using Lingo.dev AI translation + - Maintains consistency with source documentation + + ## Test Plan + - [ ] Verify translated content accuracy + - [ ] Check that all links and references work correctly + - [ ] Ensure formatting and structure are preserved + branch: auto-translations + base: ${{ github.base_ref }} + labels: | + i18n + auto-generated + + verify-translations: + needs: translate + runs-on: ubuntu-latest + if: always() # Run even if translation fails + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: | + cd apps/docs + bun install + + - name: Build documentation to verify translations + run: | + cd apps/docs + bun run build + + - name: Report translation status + run: | + cd apps/docs + echo "## Translation Status Report" >> $GITHUB_STEP_SUMMARY + + en_count=$(find content/docs/en -name "*.mdx" | wc -l) + es_count=$(find content/docs/es -name "*.mdx" 2>/dev/null | wc -l || echo 0) + fr_count=$(find content/docs/fr -name "*.mdx" 2>/dev/null | wc -l || echo 0) + zh_count=$(find content/docs/zh -name "*.mdx" 2>/dev/null | wc -l || echo 0) + + es_percentage=$((es_count * 100 / en_count)) + fr_percentage=$((fr_count * 100 / en_count)) + zh_percentage=$((zh_count * 100 / en_count)) + + echo "- **🇪🇸 Spanish**: $es_count/$en_count files ($es_percentage%)" >> $GITHUB_STEP_SUMMARY + echo "- **🇫🇷 French**: $fr_count/$en_count files ($fr_percentage%)" >> $GITHUB_STEP_SUMMARY + echo "- **🇨🇳 Chinese**: $zh_count/$en_count files ($zh_percentage%)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index d486100979..a6ec4da9ac 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ start-collector.sh .vscode ## Helm Chart Tests -helm/sim/test \ No newline at end of file +helm/sim/test +i18n.cache diff --git a/apps/docs/app/(docs)/[[...slug]]/layout.tsx b/apps/docs/app/(docs)/[[...slug]]/layout.tsx deleted file mode 100644 index 257c2d8c27..0000000000 --- a/apps/docs/app/(docs)/[[...slug]]/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { ReactNode } from 'react' - -export default function SlugLayout({ children }: { children: ReactNode }) { - return children -} diff --git a/apps/docs/app/(docs)/[[...slug]]/page.tsx b/apps/docs/app/(docs)/[[...slug]]/page.tsx deleted file mode 100644 index 5af882aa90..0000000000 --- a/apps/docs/app/(docs)/[[...slug]]/page.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import defaultMdxComponents from 'fumadocs-ui/mdx' -import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' -import { notFound } from 'next/navigation' -import { source } from '@/lib/source' - -export const dynamic = 'force-dynamic' - -export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { - const params = await props.params - const page = source.getPage(params.slug) - if (!page) notFound() - - const MDX = page.data.body - - return ( - On this page, - single: false, - }} - article={{ - className: 'scroll-smooth max-sm:pb-16', - }} - tableOfContentPopover={{ - style: 'clerk', - enabled: true, - }} - footer={{ - enabled: false, - }} - > - {page.data.title} - {page.data.description} - - - - - ) -} - -export async function generateStaticParams() { - return source.generateParams() -} - -export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { - const params = await props.params - const page = source.getPage(params.slug) - if (!page) notFound() - - return { - title: page.data.title, - description: page.data.description, - } -} diff --git a/apps/docs/app/(docs)/layout.tsx b/apps/docs/app/(docs)/layout.tsx deleted file mode 100644 index 18bf0fac61..0000000000 --- a/apps/docs/app/(docs)/layout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { ReactNode } from 'react' -import { DocsLayout } from 'fumadocs-ui/layouts/docs' -import { ExternalLink, GithubIcon } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { source } from '@/lib/source' - -const GitHubLink = () => ( -
- - - -
-) - -export default function Layout({ children }: { children: ReactNode }) { - return ( - <> - - Sim - - ), - }} - links={[ - { - text: 'Visit Sim', - url: 'https://sim.ai', - icon: , - }, - ]} - sidebar={{ - defaultOpenLevel: 0, - collapsible: true, - footer: null, - banner: null, - }} - > - {children} - - - - ) -} diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx new file mode 100644 index 0000000000..5dd543dcb3 --- /dev/null +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -0,0 +1,161 @@ +import { findNeighbour } from 'fumadocs-core/server' +import defaultMdxComponents from 'fumadocs-ui/mdx' +import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { StructuredData } from '@/components/structured-data' +import { source } from '@/lib/source' + +export const dynamic = 'force-dynamic' + +export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) { + const params = await props.params + const page = source.getPage(params.slug, params.lang) + if (!page) notFound() + + const MDX = page.data.body + const baseUrl = 'https://docs.sim.ai' + + const pageTreeRecord = source.pageTree as Record + const pageTree = + pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0] + const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null + + const CustomFooter = () => ( +
+ {neighbours?.previous ? ( + + + {neighbours.previous.name} + + ) : ( +
+ )} + + {neighbours?.next ? ( + + {neighbours.next.name} + + + ) : ( +
+ )} +
+ ) + + return ( + <> + + On this page
, + single: false, + }} + article={{ + className: 'scroll-smooth max-sm:pb-16', + }} + tableOfContentPopover={{ + style: 'clerk', + enabled: true, + }} + footer={{ + enabled: true, + component: , + }} + > + {page.data.title} + {page.data.description} + + + + + + ) +} + +export async function generateStaticParams() { + return source.generateParams() +} + +export async function generateMetadata(props: { + params: Promise<{ slug?: string[]; lang: string }> +}) { + const params = await props.params + const page = source.getPage(params.slug, params.lang) + if (!page) notFound() + + const baseUrl = 'https://docs.sim.ai' + const fullUrl = `${baseUrl}${page.url}` + + return { + title: page.data.title, + description: + page.data.description || 'Sim visual workflow builder for AI applications documentation', + keywords: [ + 'AI workflow builder', + 'visual workflow editor', + 'AI automation', + 'workflow automation', + 'AI agents', + 'no-code AI', + 'drag and drop workflows', + page.data.title?.toLowerCase().split(' '), + ] + .flat() + .filter(Boolean), + authors: [{ name: 'Sim Team' }], + category: 'Developer Tools', + openGraph: { + title: page.data.title, + description: + page.data.description || 'Sim visual workflow builder for AI applications documentation', + url: fullUrl, + siteName: 'Sim Documentation', + type: 'article', + locale: params.lang, + alternateLocale: ['en', 'fr', 'zh'].filter((lang) => lang !== params.lang), + }, + twitter: { + card: 'summary', + title: page.data.title, + description: + page.data.description || 'Sim visual workflow builder for AI applications documentation', + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + canonical: fullUrl, + alternates: { + canonical: fullUrl, + languages: { + en: `${baseUrl}/en${page.url.replace(`/${params.lang}`, '')}`, + fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`, + zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`, + }, + }, + } +} diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx new file mode 100644 index 0000000000..dc071493f3 --- /dev/null +++ b/apps/docs/app/[lang]/layout.tsx @@ -0,0 +1,99 @@ +import type { ReactNode } from 'react' +import { defineI18nUI } from 'fumadocs-ui/i18n' +import { DocsLayout } from 'fumadocs-ui/layouts/docs' +import { RootProvider } from 'fumadocs-ui/provider' +import { ExternalLink, GithubIcon } from 'lucide-react' +import { Inter } from 'next/font/google' +import Image from 'next/image' +import Link from 'next/link' +import { LanguageDropdown } from '@/components/ui/language-dropdown' +import { i18n } from '@/lib/i18n' +import { source } from '@/lib/source' +import '../global.css' +import { Analytics } from '@vercel/analytics/next' + +const inter = Inter({ + subsets: ['latin'], +}) + +const { provider } = defineI18nUI(i18n, { + translations: { + en: { + displayName: 'English', + }, + es: { + displayName: 'Español', + }, + fr: { + displayName: 'Français', + }, + zh: { + displayName: '简体中文', + }, + }, +}) + +const GitHubLink = () => ( +
+ + + +
+) + +type LayoutProps = { + children: ReactNode + params: Promise<{ lang: string }> +} + +export default async function Layout({ children, params }: LayoutProps) { + const { lang } = await params + + return ( + + + + + Sim + +
+ ), + }} + links={[ + { + text: 'Visit Sim', + url: 'https://sim.ai', + icon: , + }, + ]} + sidebar={{ + defaultOpenLevel: 0, + collapsible: true, + footer: null, + banner: null, + }} + > + {children} + + + + + + + ) +} diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index beb280c968..c0028d90b3 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,30 +1,32 @@ import type { ReactNode } from 'react' -import { RootProvider } from 'fumadocs-ui/provider' -import { Inter } from 'next/font/google' -import './global.css' -import { Analytics } from '@vercel/analytics/next' -const inter = Inter({ - subsets: ['latin'], -}) - -export default function Layout({ children }: { children: ReactNode }) { - return ( - - - - {children} - - - - - ) +export default function RootLayout({ children }: { children: ReactNode }) { + return children } export const metadata = { - title: 'Sim', + metadataBase: new URL('https://docs.sim.ai'), + title: { + default: 'Sim Documentation - Visual Workflow Builder for AI Applications', + template: '%s', + }, description: - 'Build agents in seconds with a drag and drop workflow builder. Access comprehensive documentation to help you create efficient workflows and maximize your automation capabilities.', + 'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.', + keywords: [ + 'AI workflow builder', + 'visual workflow editor', + 'AI automation', + 'workflow automation', + 'AI agents', + 'no-code AI', + 'drag and drop workflows', + 'AI integrations', + 'workflow canvas', + 'AI development platform', + ], + authors: [{ name: 'Sim Team', url: 'https://sim.ai' }], + category: 'Developer Tools', + classification: 'Developer Documentation', manifest: '/favicon/site.webmanifest', icons: { icon: [ @@ -39,4 +41,40 @@ export const metadata = { statusBarStyle: 'default', title: 'Sim Docs', }, + openGraph: { + type: 'website', + locale: 'en_US', + alternateLocale: ['fr_FR', 'zh_CN'], + url: 'https://docs.sim.ai', + siteName: 'Sim Documentation', + title: 'Sim Documentation - Visual Workflow Builder for AI Applications', + description: + 'Comprehensive documentation for Sim - the visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.', + }, + twitter: { + card: 'summary', + title: 'Sim Documentation - Visual Workflow Builder for AI Applications', + description: + 'Comprehensive documentation for Sim - the visual workflow builder for AI applications.', + creator: '@sim_ai', + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + alternates: { + canonical: 'https://docs.sim.ai', + languages: { + en: '/en', + fr: '/fr', + zh: '/zh', + }, + }, } diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 6496e0d5ad..fb60ef33f4 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -1,7 +1,6 @@ import { getLLMText } from '@/lib/llms' import { source } from '@/lib/source' -// cached forever export const revalidate = false export async function GET() { diff --git a/apps/docs/app/robots.txt/route.ts b/apps/docs/app/robots.txt/route.ts new file mode 100644 index 0000000000..46e1a68d4a --- /dev/null +++ b/apps/docs/app/robots.txt/route.ts @@ -0,0 +1,58 @@ +export const revalidate = false + +export async function GET() { + const baseUrl = 'https://docs.sim.ai' + + const robotsTxt = `# Robots.txt for Sim Documentation +# Generated on ${new Date().toISOString()} + +User-agent: * +Allow: / + +# Allow all well-behaved crawlers +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +# AI and LLM crawlers +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: Claude-Web +Allow: / + +# Disallow admin and internal paths (if any exist) +Disallow: /.next/ +Disallow: /api/internal/ +Disallow: /_next/static/ +Disallow: /admin/ + +# Allow but don't prioritize these +Allow: /api/search +Allow: /llms.txt +Allow: /llms.mdx/ + +# Sitemaps +Sitemap: ${baseUrl}/sitemap.xml + +# Additional resources for AI indexing +# See https://github.com/AnswerDotAI/llms-txt for more info +# LLM-friendly content available at: ${baseUrl}/llms.txt` + + return new Response(robotsTxt, { + headers: { + 'Content-Type': 'text/plain', + }, + }) +} diff --git a/apps/docs/app/sitemap.xml/route.ts b/apps/docs/app/sitemap.xml/route.ts new file mode 100644 index 0000000000..00f36ac09f --- /dev/null +++ b/apps/docs/app/sitemap.xml/route.ts @@ -0,0 +1,54 @@ +import { i18n } from '@/lib/i18n' +import { source } from '@/lib/source' + +export const revalidate = false + +export async function GET() { + const baseUrl = 'https://docs.sim.ai' + + const allPages = source.getPages() + + const urls = allPages + .flatMap((page) => { + const urlWithoutLang = page.url.replace(/^\/[a-z]{2}\//, '/') + + return i18n.languages.map((lang) => { + const url = + lang === i18n.defaultLanguage + ? `${baseUrl}${urlWithoutLang}` + : `${baseUrl}/${lang}${urlWithoutLang}` + + return ` + ${url} + ${new Date().toISOString().split('T')[0]} + weekly + ${urlWithoutLang === '/introduction' ? '1.0' : '0.8'} + ${i18n.languages.length > 1 ? generateAlternateLinks(baseUrl, urlWithoutLang) : ''} + ` + }) + }) + .join('\n') + + const sitemap = ` + +${urls} +` + + return new Response(sitemap, { + headers: { + 'Content-Type': 'application/xml', + }, + }) +} + +function generateAlternateLinks(baseUrl: string, urlWithoutLang: string): string { + return i18n.languages + .map((lang) => { + const url = + lang === i18n.defaultLanguage + ? `${baseUrl}${urlWithoutLang}` + : `${baseUrl}/${lang}${urlWithoutLang}` + return ` ` + }) + .join('\n') +} diff --git a/apps/docs/components/structured-data.tsx b/apps/docs/components/structured-data.tsx new file mode 100644 index 0000000000..c09e0136e3 --- /dev/null +++ b/apps/docs/components/structured-data.tsx @@ -0,0 +1,174 @@ +import Script from 'next/script' + +interface StructuredDataProps { + title: string + description: string + url: string + lang: string + dateModified?: string + breadcrumb?: Array<{ name: string; url: string }> +} + +export function StructuredData({ + title, + description, + url, + lang, + dateModified, + breadcrumb, +}: StructuredDataProps) { + const baseUrl = 'https://docs.sim.ai' + + const articleStructuredData = { + '@context': 'https://schema.org', + '@type': 'TechArticle', + headline: title, + description: description, + url: url, + datePublished: dateModified || new Date().toISOString(), + dateModified: dateModified || new Date().toISOString(), + author: { + '@type': 'Organization', + name: 'Sim Team', + url: baseUrl, + }, + publisher: { + '@type': 'Organization', + name: 'Sim', + url: baseUrl, + logo: { + '@type': 'ImageObject', + url: `${baseUrl}/static/logo.png`, + }, + }, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': url, + }, + inLanguage: lang, + isPartOf: { + '@type': 'WebSite', + name: 'Sim Documentation', + url: baseUrl, + }, + potentialAction: { + '@type': 'ReadAction', + target: url, + }, + } + + const breadcrumbStructuredData = breadcrumb && { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: breadcrumb.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: item.url, + })), + } + + const websiteStructuredData = url === baseUrl && { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Sim Documentation', + url: baseUrl, + description: + 'Comprehensive documentation for Sim visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines.', + publisher: { + '@type': 'Organization', + name: 'Sim', + url: baseUrl, + }, + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: `${baseUrl}/search?q={search_term_string}`, + }, + 'query-input': 'required name=search_term_string', + }, + inLanguage: ['en', 'fr', 'zh'], + } + + const faqStructuredData = title.toLowerCase().includes('faq') && { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: [], + } + + const softwareStructuredData = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'Sim', + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Any', + description: + 'Visual workflow builder for AI applications. Create powerful AI agents, automation workflows, and data processing pipelines by connecting blocks on a canvas—no coding required.', + url: baseUrl, + author: { + '@type': 'Organization', + name: 'Sim Team', + }, + offers: { + '@type': 'Offer', + category: 'Developer Tools', + }, + featureList: [ + 'Visual workflow builder with drag-and-drop interface', + 'AI agent creation and automation', + '80+ built-in integrations', + 'Real-time team collaboration', + 'Multiple deployment options', + 'Custom integrations via MCP protocol', + ], + } + + return ( + <> +