diff --git a/apps/sim/app/(landing)/blog/authors/[id]/page.tsx b/apps/sim/app/(landing)/blog/authors/[id]/page.tsx index 3362e3ee917..bbbc8a55913 100644 --- a/apps/sim/app/(landing)/blog/authors/[id]/page.tsx +++ b/apps/sim/app/(landing)/blog/authors/[id]/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import Image from 'next/image' import Link from 'next/link' import { getAllPostMeta } from '@/lib/blog/registry' +import { SITE_URL } from '@/lib/core/utils/urls' export const revalidate = 3600 @@ -17,11 +18,11 @@ export async function generateMetadata({ return { title: `${name} — Sim Blog`, description: `Read articles by ${name} on the Sim blog.`, - alternates: { canonical: `https://sim.ai/blog/authors/${id}` }, + alternates: { canonical: `${SITE_URL}/blog/authors/${id}` }, openGraph: { title: `${name} — Sim Blog`, description: `Read articles by ${name} on the Sim blog.`, - url: `https://sim.ai/blog/authors/${id}`, + url: `${SITE_URL}/blog/authors/${id}`, siteName: 'Sim', type: 'profile', ...(author?.avatarUrl @@ -55,25 +56,25 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str { '@type': 'Person', name: author.name, - url: `https://sim.ai/blog/authors/${author.id}`, + url: `${SITE_URL}/blog/authors/${author.id}`, sameAs: author.url ? [author.url] : [], image: author.avatarUrl, worksFor: { '@type': 'Organization', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, }, }, { '@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: 1, name: 'Home', item: SITE_URL }, + { '@type': 'ListItem', position: 2, name: 'Blog', item: `${SITE_URL}/blog` }, { '@type': 'ListItem', position: 3, name: author.name, - item: `https://sim.ai/blog/authors/${author.id}`, + item: `${SITE_URL}/blog/authors/${author.id}`, }, ], }, diff --git a/apps/sim/app/(landing)/blog/layout.tsx b/apps/sim/app/(landing)/blog/layout.tsx index 512f41a32ee..96b81a7dca5 100644 --- a/apps/sim/app/(landing)/blog/layout.tsx +++ b/apps/sim/app/(landing)/blog/layout.tsx @@ -1,4 +1,5 @@ import { getNavBlogPosts } from '@/lib/blog/registry' +import { SITE_URL } from '@/lib/core/utils/urls' import Footer from '@/app/(landing)/components/footer/footer' import Navbar from '@/app/(landing)/components/navbar/navbar' @@ -8,10 +9,10 @@ export default async function StudioLayout({ children }: { children: React.React '@context': 'https://schema.org', '@type': 'Organization', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, description: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents.', - logo: 'https://sim.ai/logo/primary/small.png', + logo: `${SITE_URL}/logo/primary/small.png`, sameAs: [ 'https://x.com/simdotai', 'https://github.com/simstudioai/sim', @@ -23,7 +24,7 @@ export default async function StudioLayout({ children }: { children: React.React '@context': 'https://schema.org', '@type': 'WebSite', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, } return ( diff --git a/apps/sim/app/(landing)/blog/page.tsx b/apps/sim/app/(landing)/blog/page.tsx index a7339cc76ad..f12f73ed253 100644 --- a/apps/sim/app/(landing)/blog/page.tsx +++ b/apps/sim/app/(landing)/blog/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import { Badge } from '@/components/emcn' import { getAllPostMeta } from '@/lib/blog/registry' import { buildCollectionPageJsonLd } from '@/lib/blog/seo' +import { SITE_URL } from '@/lib/core/utils/urls' export async function generateMetadata({ searchParams, @@ -26,7 +27,7 @@ export async function generateMetadata({ 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}` : ''}` + const canonical = `${SITE_URL}/blog${qs ? `?${qs}` : ''}` return { title, @@ -41,7 +42,7 @@ export async function generateMetadata({ type: 'website', images: [ { - url: 'https://sim.ai/logo/primary/medium.png', + url: `${SITE_URL}/logo/primary/medium.png`, width: 1200, height: 630, alt: 'Sim Blog', diff --git a/apps/sim/app/(landing)/blog/rss.xml/route.ts b/apps/sim/app/(landing)/blog/rss.xml/route.ts index fdabfce7ebc..6460e032216 100644 --- a/apps/sim/app/(landing)/blog/rss.xml/route.ts +++ b/apps/sim/app/(landing)/blog/rss.xml/route.ts @@ -1,12 +1,13 @@ import { NextResponse } from 'next/server' import { getAllPostMeta } from '@/lib/blog/registry' +import { SITE_URL } from '@/lib/core/utils/urls' export const revalidate = 3600 export async function GET() { const posts = await getAllPostMeta() const items = posts.slice(0, 50) - const site = 'https://sim.ai' + const site = SITE_URL const lastBuildDate = items.length > 0 ? new Date(items[0].date).toUTCString() : new Date().toUTCString() diff --git a/apps/sim/app/(landing)/blog/sitemap-images.xml/route.ts b/apps/sim/app/(landing)/blog/sitemap-images.xml/route.ts index 7fa302f299d..c40833c02c2 100644 --- a/apps/sim/app/(landing)/blog/sitemap-images.xml/route.ts +++ b/apps/sim/app/(landing)/blog/sitemap-images.xml/route.ts @@ -1,11 +1,12 @@ import { NextResponse } from 'next/server' import { getAllPostMeta } from '@/lib/blog/registry' +import { SITE_URL } from '@/lib/core/utils/urls' export const revalidate = 3600 export async function GET() { const posts = await getAllPostMeta() - const base = 'https://sim.ai' + const base = SITE_URL const xml = ` ${posts diff --git a/apps/sim/app/(landing)/blog/tags/page.tsx b/apps/sim/app/(landing)/blog/tags/page.tsx index 1b5ccceea30..b18cff5a46d 100644 --- a/apps/sim/app/(landing)/blog/tags/page.tsx +++ b/apps/sim/app/(landing)/blog/tags/page.tsx @@ -1,15 +1,16 @@ import type { Metadata } from 'next' import Link from 'next/link' import { getAllTags } from '@/lib/blog/registry' +import { SITE_URL } from '@/lib/core/utils/urls' 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' }, + alternates: { canonical: `${SITE_URL}/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', + url: `${SITE_URL}/blog/tags`, siteName: 'Sim', locale: 'en_US', type: 'website', @@ -26,9 +27,9 @@ 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' }, + { '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL }, + { '@type': 'ListItem', position: 2, name: 'Blog', item: `${SITE_URL}/blog` }, + { '@type': 'ListItem', position: 3, name: 'Tags', item: `${SITE_URL}/blog/tags` }, ], } diff --git a/apps/sim/app/(landing)/components/structured-data.tsx b/apps/sim/app/(landing)/components/structured-data.tsx index b03c4fb45e9..5a55b1c1c5d 100644 --- a/apps/sim/app/(landing)/components/structured-data.tsx +++ b/apps/sim/app/(landing)/components/structured-data.tsx @@ -1,3 +1,5 @@ +import { SITE_URL } from '@/lib/core/utils/urls' + /** * JSON-LD structured data for the landing page. * @@ -23,22 +25,22 @@ export default function StructuredData() { '@graph': [ { '@type': 'Organization', - '@id': 'https://sim.ai/#organization', + '@id': `${SITE_URL}/#organization`, name: 'Sim', alternateName: 'Sim Studio', description: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.', - url: 'https://sim.ai', + url: SITE_URL, logo: { '@type': 'ImageObject', - '@id': 'https://sim.ai/#logo', - url: 'https://sim.ai/logo/b%26w/text/b%26w.svg', - contentUrl: 'https://sim.ai/logo/b%26w/text/b%26w.svg', + '@id': `${SITE_URL}/#logo`, + url: `${SITE_URL}/logo/b%26w/text/b%26w.svg`, + contentUrl: `${SITE_URL}/logo/b%26w/text/b%26w.svg`, width: 49.78314, height: 24.276, caption: 'Sim Logo', }, - image: { '@id': 'https://sim.ai/#logo' }, + image: { '@id': `${SITE_URL}/#logo` }, sameAs: [ 'https://x.com/simdotai', 'https://github.com/simstudioai/sim', @@ -53,44 +55,42 @@ export default function StructuredData() { }, { '@type': 'WebSite', - '@id': 'https://sim.ai/#website', - url: 'https://sim.ai', + '@id': `${SITE_URL}/#website`, + url: SITE_URL, name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', description: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM. Join 100,000+ builders.', - publisher: { '@id': 'https://sim.ai/#organization' }, + publisher: { '@id': `${SITE_URL}/#organization` }, inLanguage: 'en-US', }, { '@type': 'WebPage', - '@id': 'https://sim.ai/#webpage', - url: 'https://sim.ai', + '@id': `${SITE_URL}/#webpage`, + url: SITE_URL, name: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', - isPartOf: { '@id': 'https://sim.ai/#website' }, - about: { '@id': 'https://sim.ai/#software' }, + isPartOf: { '@id': `${SITE_URL}/#website` }, + about: { '@id': `${SITE_URL}/#software` }, datePublished: '2024-01-01T00:00:00+00:00', dateModified: new Date().toISOString(), description: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work.', - breadcrumb: { '@id': 'https://sim.ai/#breadcrumb' }, + breadcrumb: { '@id': `${SITE_URL}/#breadcrumb` }, inLanguage: 'en-US', speakable: { '@type': 'SpeakableSpecification', cssSelector: ['#hero-heading', '[id="hero"] p'], }, - potentialAction: [{ '@type': 'ReadAction', target: ['https://sim.ai'] }], + potentialAction: [{ '@type': 'ReadAction', target: [SITE_URL] }], }, { '@type': 'BreadcrumbList', - '@id': 'https://sim.ai/#breadcrumb', - itemListElement: [ - { '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' }, - ], + '@id': `${SITE_URL}/#breadcrumb`, + itemListElement: [{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL }], }, { '@type': 'WebApplication', - '@id': 'https://sim.ai/#software', - url: 'https://sim.ai', + '@id': `${SITE_URL}/#software`, + url: SITE_URL, name: 'Sim — The AI Workspace', description: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code. Trusted by over 100,000 builders. SOC2 compliant.', @@ -98,7 +98,7 @@ export default function StructuredData() { applicationSubCategory: 'AI Workspace', operatingSystem: 'Web', browserRequirements: 'Requires a modern browser with JavaScript enabled', - installUrl: 'https://sim.ai/signup', + installUrl: `${SITE_URL}/signup`, offers: [ { '@type': 'Offer', @@ -175,16 +175,16 @@ export default function StructuredData() { }, { '@type': 'SoftwareSourceCode', - '@id': 'https://sim.ai/#source', + '@id': `${SITE_URL}/#source`, codeRepository: 'https://github.com/simstudioai/sim', programmingLanguage: ['TypeScript', 'Python'], runtimePlatform: 'Node.js', license: 'https://opensource.org/licenses/Apache-2.0', - isPartOf: { '@id': 'https://sim.ai/#software' }, + isPartOf: { '@id': `${SITE_URL}/#software` }, }, { '@type': 'FAQPage', - '@id': 'https://sim.ai/#faq', + '@id': `${SITE_URL}/#faq`, mainEntity: [ { '@type': 'Question', diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index e93bf9c73fe..d48ea6f4054 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -2,7 +2,7 @@ import type { Metadata } from 'next' import Image from 'next/image' import Link from 'next/link' import { notFound } from 'next/navigation' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import { IntegrationCtaButton } from '@/app/(landing)/integrations/[slug]/components/integration-cta-button' import { IntegrationFAQ } from '@/app/(landing)/integrations/[slug]/components/integration-faq' import { TemplateCardButton } from '@/app/(landing)/integrations/[slug]/components/template-card-button' @@ -14,7 +14,7 @@ import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/templat const allIntegrations = integrations as Integration[] const INTEGRATION_COUNT = allIntegrations.length -const baseUrl = getBaseUrl() +const baseUrl = SITE_URL /** Fast O(1) lookups — avoids repeated linear scans inside render loops. */ const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) diff --git a/apps/sim/app/(landing)/integrations/layout.tsx b/apps/sim/app/(landing)/integrations/layout.tsx index 23614abe122..231771091c7 100644 --- a/apps/sim/app/(landing)/integrations/layout.tsx +++ b/apps/sim/app/(landing)/integrations/layout.tsx @@ -1,11 +1,11 @@ import { getNavBlogPosts } from '@/lib/blog/registry' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import Footer from '@/app/(landing)/components/footer/footer' import Navbar from '@/app/(landing)/components/navbar/navbar' export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) { const blogPosts = await getNavBlogPosts() - const url = getBaseUrl() + const url = SITE_URL const orgJsonLd = { '@context': 'https://schema.org', '@type': 'Organization', diff --git a/apps/sim/app/(landing)/integrations/page.tsx b/apps/sim/app/(landing)/integrations/page.tsx index 60927489eeb..3340ba7f271 100644 --- a/apps/sim/app/(landing)/integrations/page.tsx +++ b/apps/sim/app/(landing)/integrations/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' import { Badge } from '@/components/emcn' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import { IntegrationCard } from './components/integration-card' import { IntegrationGrid } from './components/integration-grid' import { RequestIntegrationModal } from './components/request-integration-modal' @@ -18,7 +18,7 @@ const INTEGRATION_COUNT = allIntegrations.length */ const TOP_NAMES = [...new Set(POPULAR_WORKFLOWS.flatMap((p) => [p.from, p.to]))].slice(0, 6) -const baseUrl = getBaseUrl() +const baseUrl = SITE_URL /** Curated featured integrations — high-recognition services shown as cards. */ const FEATURED_SLUGS = ['slack', 'notion', 'github', 'gmail'] as const diff --git a/apps/sim/app/(landing)/layout.tsx b/apps/sim/app/(landing)/layout.tsx index bb6ee982754..3b10895f16e 100644 --- a/apps/sim/app/(landing)/layout.tsx +++ b/apps/sim/app/(landing)/layout.tsx @@ -1,9 +1,10 @@ import type { Metadata } from 'next' +import { SITE_URL } from '@/lib/core/utils/urls' import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' import { season } from '@/app/_styles/fonts/season/season' export const metadata: Metadata = { - metadataBase: new URL('https://sim.ai'), + metadataBase: new URL(SITE_URL), manifest: '/manifest.webmanifest', icons: { icon: [{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' }], diff --git a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx index 7334c689cbb..c8ab7d8c423 100644 --- a/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx +++ b/apps/sim/app/(landing)/models/[provider]/[model]/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next' import Link from 'next/link' import { notFound } from 'next/navigation' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import { LandingFAQ } from '@/app/(landing)/components/landing-faq' import { FeaturedModelCard, ProviderIcon } from '@/app/(landing)/models/components/model-primitives' import { @@ -18,7 +18,7 @@ import { getRelatedModels, } from '@/app/(landing)/models/utils' -const baseUrl = getBaseUrl() +const baseUrl = SITE_URL export async function generateStaticParams() { return ALL_CATALOG_MODELS.map((model) => ({ @@ -221,7 +221,7 @@ export default async function ModelPage({
Build with this model diff --git a/apps/sim/app/(landing)/models/[provider]/page.tsx b/apps/sim/app/(landing)/models/[provider]/page.tsx index 19e9fa730e3..ae2acbe2734 100644 --- a/apps/sim/app/(landing)/models/[provider]/page.tsx +++ b/apps/sim/app/(landing)/models/[provider]/page.tsx @@ -2,7 +2,7 @@ import type { Metadata } from 'next' import Link from 'next/link' import { notFound } from 'next/navigation' import { Badge } from '@/components/emcn' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import { LandingFAQ } from '@/app/(landing)/components/landing-faq' import { ChevronArrow, @@ -20,7 +20,7 @@ import { TOP_MODEL_PROVIDERS, } from '@/app/(landing)/models/utils' -const baseUrl = getBaseUrl() +const baseUrl = SITE_URL export async function generateStaticParams() { return MODEL_PROVIDERS_WITH_CATALOGS.map((provider) => ({ diff --git a/apps/sim/app/(landing)/models/layout.tsx b/apps/sim/app/(landing)/models/layout.tsx index f211da54610..672632f70b4 100644 --- a/apps/sim/app/(landing)/models/layout.tsx +++ b/apps/sim/app/(landing)/models/layout.tsx @@ -1,11 +1,11 @@ import { getNavBlogPosts } from '@/lib/blog/registry' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import Footer from '@/app/(landing)/components/footer/footer' import Navbar from '@/app/(landing)/components/navbar/navbar' export default async function ModelsLayout({ children }: { children: React.ReactNode }) { const blogPosts = await getNavBlogPosts() - const url = getBaseUrl() + const url = SITE_URL const orgJsonLd = { '@context': 'https://schema.org', '@type': 'Organization', diff --git a/apps/sim/app/(landing)/models/page.tsx b/apps/sim/app/(landing)/models/page.tsx index ed41353f74f..dd01727fde7 100644 --- a/apps/sim/app/(landing)/models/page.tsx +++ b/apps/sim/app/(landing)/models/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' import { Badge } from '@/components/emcn' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import { LandingFAQ } from '@/app/(landing)/components/landing-faq' import { ModelComparisonCharts } from '@/app/(landing)/models/components/model-comparison-charts' import { ModelDirectory } from '@/app/(landing)/models/components/model-directory' @@ -17,7 +17,7 @@ import { TOTAL_MODELS, } from '@/app/(landing)/models/utils' -const baseUrl = getBaseUrl() +const baseUrl = SITE_URL const faqItems = [ { diff --git a/apps/sim/app/(landing)/partners/page.tsx b/apps/sim/app/(landing)/partners/page.tsx index ccdda2603ee..e6d26f0d3b4 100644 --- a/apps/sim/app/(landing)/partners/page.tsx +++ b/apps/sim/app/(landing)/partners/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import { getNavBlogPosts } from '@/lib/blog/registry' +import { SITE_URL } from '@/lib/core/utils/urls' import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' import { season } from '@/app/_styles/fonts/season/season' import Footer from '@/app/(landing)/components/footer/footer' @@ -9,7 +10,7 @@ export const metadata: Metadata = { title: 'Partner Program', description: "Join the Sim partner program. Build, deploy, and sell AI agent solutions powered by Sim's AI workspace. Earn your certification through Sim Academy.", - metadataBase: new URL('https://sim.ai'), + metadataBase: new URL(SITE_URL), openGraph: { title: 'Partner Program | Sim', description: 'Join the Sim partner program.', diff --git a/apps/sim/app/(landing)/seo.test.ts b/apps/sim/app/(landing)/seo.test.ts new file mode 100644 index 00000000000..cb7b207af05 --- /dev/null +++ b/apps/sim/app/(landing)/seo.test.ts @@ -0,0 +1,127 @@ +/** + * @vitest-environment node + */ +import fs from 'fs' +import path from 'path' +import { describe, expect, it } from 'vitest' +import { SITE_URL } from '@/lib/core/utils/urls' + +const SIM_ROOT = path.resolve(__dirname, '..', '..') +const APP_DIR = path.resolve(SIM_ROOT, 'app') +const LANDING_DIR = path.resolve(APP_DIR, '(landing)') + +/** + * All directories containing public-facing pages or SEO-relevant code. + * Non-marketing app routes (workspace, chat, form) are excluded — + * they legitimately use getBaseUrl() for dynamic, env-dependent URLs. + */ +const SEO_SCAN_DIRS = [ + LANDING_DIR, + path.resolve(APP_DIR, 'changelog'), + path.resolve(APP_DIR, 'changelog.xml'), + path.resolve(APP_DIR, 'academy'), + path.resolve(SIM_ROOT, 'lib', 'blog'), + path.resolve(SIM_ROOT, 'content', 'blog'), +] + +const SEO_SCAN_INDIVIDUAL_FILES = [ + path.resolve(APP_DIR, 'page.tsx'), + path.resolve(SIM_ROOT, 'ee', 'whitelabeling', 'metadata.ts'), +] + +function collectFiles(dir: string, exts: string[]): string[] { + const results: string[] = [] + if (!fs.existsSync(dir)) return results + + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + results.push(...collectFiles(full, exts)) + } else if (exts.some((ext) => entry.name.endsWith(ext)) && !entry.name.includes('.test.')) { + results.push(full) + } + } + return results +} + +function getAllSeoFiles(exts: string[]): string[] { + const files: string[] = [] + for (const dir of SEO_SCAN_DIRS) { + files.push(...collectFiles(dir, exts)) + } + for (const file of SEO_SCAN_INDIVIDUAL_FILES) { + if (fs.existsSync(file)) files.push(file) + } + return files +} + +describe('SEO canonical URLs', () => { + it('SITE_URL equals https://www.sim.ai', () => { + expect(SITE_URL).toBe('https://www.sim.ai') + }) + + it('public pages do not hardcode https://sim.ai (without www)', () => { + const files = getAllSeoFiles(['.ts', '.tsx', '.mdx']) + const violations: string[] = [] + + for (const file of files) { + const content = fs.readFileSync(file, 'utf-8') + const lines = content.split('\n') + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const hasBareSimAi = + line.includes("'https://sim.ai'") || + line.includes("'https://sim.ai/") || + line.includes('"https://sim.ai"') || + line.includes('"https://sim.ai/') || + line.includes('`https://sim.ai/') || + line.includes('`https://sim.ai`') || + line.includes('canonical: https://sim.ai/') + + if (!hasBareSimAi) continue + + const isAllowlisted = + line.includes('https://sim.ai/careers') || line.includes('https://sim.ai/discord') + + if (isAllowlisted) continue + + const rel = path.relative(SIM_ROOT, file) + violations.push(`${rel}:${i + 1}: ${line.trim()}`) + } + } + + expect( + violations, + `Found hardcoded https://sim.ai (without www):\n${violations.join('\n')}` + ).toHaveLength(0) + }) + + it('public pages do not use getBaseUrl() for SEO metadata', () => { + const files = getAllSeoFiles(['.ts', '.tsx']) + const violations: string[] = [] + + for (const file of files) { + const content = fs.readFileSync(file, 'utf-8') + + if (!content.includes('getBaseUrl')) continue + + const hasMetadataExport = + content.includes('export const metadata') || + content.includes('export async function generateMetadata') + const usesGetBaseUrlInMetadata = + hasMetadataExport && + (content.includes('= getBaseUrl()') || content.includes('metadataBase: new URL(getBaseUrl')) + + if (usesGetBaseUrlInMetadata) { + const rel = path.relative(SIM_ROOT, file) + violations.push(rel) + } + } + + expect( + violations, + `Public pages should use SITE_URL for metadata, not getBaseUrl():\n${violations.join('\n')}` + ).toHaveLength(0) + }) +}) diff --git a/apps/sim/app/academy/layout.tsx b/apps/sim/app/academy/layout.tsx index 502b1d4d574..265a01e83e9 100644 --- a/apps/sim/app/academy/layout.tsx +++ b/apps/sim/app/academy/layout.tsx @@ -1,6 +1,7 @@ import type React from 'react' import type { Metadata } from 'next' import { notFound } from 'next/navigation' +import { SITE_URL } from '@/lib/core/utils/urls' // TODO: Remove notFound() call to make academy pages public once content is ready const ACADEMY_ENABLED = false @@ -12,7 +13,7 @@ export const metadata: Metadata = { }, description: 'Become a certified Sim partner — learn to build, integrate, and deploy AI workflows.', - metadataBase: new URL('https://sim.ai'), + metadataBase: new URL(SITE_URL), openGraph: { title: 'Sim Academy', description: 'Become a certified Sim partner.', diff --git a/apps/sim/app/changelog.xml/route.ts b/apps/sim/app/changelog.xml/route.ts index 5e0752056d9..9aee139447d 100644 --- a/apps/sim/app/changelog.xml/route.ts +++ b/apps/sim/app/changelog.xml/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server' +import { SITE_URL } from '@/lib/core/utils/urls' export const dynamic = 'force-static' export const revalidate = 3600 @@ -48,7 +49,7 @@ export async function GET() { Sim Changelog - https://sim.ai/changelog + ${SITE_URL}/changelog Latest changes, fixes and updates in Sim. en-us ${items} diff --git a/apps/sim/app/changelog/page.tsx b/apps/sim/app/changelog/page.tsx index c94b650667e..7b7a5a2a531 100644 --- a/apps/sim/app/changelog/page.tsx +++ b/apps/sim/app/changelog/page.tsx @@ -1,9 +1,11 @@ import type { Metadata } from 'next' +import { SITE_URL } from '@/lib/core/utils/urls' import ChangelogContent from '@/app/changelog/components/changelog-content' export const metadata: Metadata = { title: 'Changelog', description: 'Stay up-to-date with the latest features, improvements, and bug fixes in Sim.', + alternates: { canonical: `${SITE_URL}/changelog` }, openGraph: { title: 'Changelog', description: 'Stay up-to-date with the latest features, improvements, and bug fixes in Sim.', diff --git a/apps/sim/app/page.tsx b/apps/sim/app/page.tsx index f746d2b3da6..c12a4a75e3d 100644 --- a/apps/sim/app/page.tsx +++ b/apps/sim/app/page.tsx @@ -1,13 +1,11 @@ import type { Metadata } from 'next' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { SITE_URL } from '@/lib/core/utils/urls' import Landing from '@/app/(landing)/landing' export const revalidate = 3600 -const baseUrl = getBaseUrl() - export const metadata: Metadata = { - metadataBase: new URL(baseUrl), + metadataBase: new URL(SITE_URL), title: { absolute: 'Sim — The AI Workspace | Build, Deploy & Manage AI Agents', }, @@ -28,7 +26,7 @@ export const metadata: Metadata = { description: 'Sim is the open-source AI workspace where teams build, deploy, and manage AI agents. Connect 1,000+ integrations and every major LLM to create agents that automate real work — visually, conversationally, or with code.', type: 'website', - url: baseUrl, + url: SITE_URL, siteName: 'Sim', locale: 'en_US', images: [ @@ -54,10 +52,10 @@ export const metadata: Metadata = { }, }, alternates: { - canonical: baseUrl, + canonical: SITE_URL, languages: { - 'en-US': baseUrl, - 'x-default': baseUrl, + 'en-US': SITE_URL, + 'x-default': SITE_URL, }, }, robots: { diff --git a/apps/sim/content/blog/copilot/index.mdx b/apps/sim/content/blog/copilot/index.mdx index 98add964847..5e65575549d 100644 --- a/apps/sim/content/blog/copilot/index.mdx +++ b/apps/sim/content/blog/copilot/index.mdx @@ -12,7 +12,7 @@ ogImage: /blog/copilot/cover.png ogAlt: 'Sim Copilot technical overview' about: ['AI Assistants', 'Agentic Workflows', 'Retrieval Augmented Generation'] timeRequired: PT7M -canonical: https://sim.ai/blog/copilot +canonical: https://www.sim.ai/blog/copilot featured: false draft: true --- diff --git a/apps/sim/content/blog/emcn/index.mdx b/apps/sim/content/blog/emcn/index.mdx index 9dddba8244f..8b427baf27d 100644 --- a/apps/sim/content/blog/emcn/index.mdx +++ b/apps/sim/content/blog/emcn/index.mdx @@ -12,7 +12,7 @@ ogImage: /blog/emcn/cover.png ogAlt: 'Emcn design system cover' about: ['Design Systems', 'Component Libraries', 'Design Tokens', 'Accessibility'] timeRequired: PT6M -canonical: https://sim.ai/blog/emcn +canonical: https://www.sim.ai/blog/emcn featured: false draft: true --- diff --git a/apps/sim/content/blog/enterprise/index.mdx b/apps/sim/content/blog/enterprise/index.mdx index 3f57456617a..81bf3acdad4 100644 --- a/apps/sim/content/blog/enterprise/index.mdx +++ b/apps/sim/content/blog/enterprise/index.mdx @@ -12,7 +12,7 @@ ogImage: /blog/enterprise/cover.png ogAlt: 'Sim Enterprise features overview' about: ['Enterprise Software', 'Security', 'Compliance', 'Self-Hosting'] timeRequired: PT10M -canonical: https://sim.ai/blog/enterprise +canonical: https://www.sim.ai/blog/enterprise featured: true draft: false --- diff --git a/apps/sim/content/blog/executor/index.mdx b/apps/sim/content/blog/executor/index.mdx index 61c9407ee44..01b410ba57d 100644 --- a/apps/sim/content/blog/executor/index.mdx +++ b/apps/sim/content/blog/executor/index.mdx @@ -12,7 +12,7 @@ ogImage: /blog/executor/cover.png ogAlt: 'Sim Executor technical overview' about: ['Execution', 'Workflow Orchestration'] timeRequired: PT12M -canonical: https://sim.ai/blog/executor +canonical: https://www.sim.ai/blog/executor featured: false draft: false --- diff --git a/apps/sim/content/blog/mothership/index.mdx b/apps/sim/content/blog/mothership/index.mdx index 5205c6023df..ff01969d2be 100644 --- a/apps/sim/content/blog/mothership/index.mdx +++ b/apps/sim/content/blog/mothership/index.mdx @@ -8,11 +8,11 @@ authors: - emir readingTime: 10 tags: [Release, Mothership, Tables, Knowledge Base, Connectors, RAG, Sim] -ogImage: /blog/mothership/cover.png +ogImage: /blog/mothership/cover.jpg ogAlt: 'Sim v0.6 release announcement' about: ['AI Agents', 'Workflow Automation', 'Developer Tools'] timeRequired: PT10M -canonical: https://sim.ai/blog/mothership +canonical: https://www.sim.ai/blog/mothership featured: true draft: false --- diff --git a/apps/sim/content/blog/multiplayer/index.mdx b/apps/sim/content/blog/multiplayer/index.mdx index 71a48fa89fd..5d32e444a1e 100644 --- a/apps/sim/content/blog/multiplayer/index.mdx +++ b/apps/sim/content/blog/multiplayer/index.mdx @@ -9,7 +9,7 @@ authors: readingTime: 12 tags: [Multiplayer, Realtime, Collaboration, WebSockets, Architecture] ogImage: /blog/multiplayer/cover.png -canonical: https://sim.ai/blog/multiplayer +canonical: https://www.sim.ai/blog/multiplayer draft: false --- diff --git a/apps/sim/content/blog/openai-vs-n8n-vs-sim/index.mdx b/apps/sim/content/blog/openai-vs-n8n-vs-sim/index.mdx index 9026829f56f..ea21ba1fc34 100644 --- a/apps/sim/content/blog/openai-vs-n8n-vs-sim/index.mdx +++ b/apps/sim/content/blog/openai-vs-n8n-vs-sim/index.mdx @@ -9,7 +9,7 @@ authors: readingTime: 9 tags: [AI Agents, Workflow Automation, OpenAI AgentKit, n8n, Sim, MCP] ogImage: /blog/openai-vs-n8n-vs-sim/workflow.png -canonical: https://sim.ai/blog/openai-vs-n8n-vs-sim +canonical: https://www.sim.ai/blog/openai-vs-n8n-vs-sim draft: false --- diff --git a/apps/sim/content/blog/series-a/index.mdx b/apps/sim/content/blog/series-a/index.mdx index e029c884a28..ee119fb7170 100644 --- a/apps/sim/content/blog/series-a/index.mdx +++ b/apps/sim/content/blog/series-a/index.mdx @@ -13,7 +13,7 @@ ogImage: /blog/series-a/cover.png ogAlt: 'Sim team photo in front of neon logo' about: ['Artificial Intelligence', 'Agentic Workflows', 'Startups', 'Funding'] timeRequired: PT4M -canonical: https://sim.ai/blog/series-a +canonical: https://www.sim.ai/blog/series-a featured: true draft: false --- diff --git a/apps/sim/content/blog/v0-5/index.mdx b/apps/sim/content/blog/v0-5/index.mdx index b97609f41c7..b4b80137580 100644 --- a/apps/sim/content/blog/v0-5/index.mdx +++ b/apps/sim/content/blog/v0-5/index.mdx @@ -12,7 +12,7 @@ ogImage: /blog/v0-5/cover.png ogAlt: 'Sim v0.5 release announcement' about: ['AI Agents', 'Workflow Automation', 'Developer Tools'] timeRequired: PT8M -canonical: https://sim.ai/blog/v0-5 +canonical: https://www.sim.ai/blog/v0-5 featured: true draft: false --- diff --git a/apps/sim/ee/whitelabeling/metadata.ts b/apps/sim/ee/whitelabeling/metadata.ts index cfaefd63f47..1048f56ed62 100644 --- a/apps/sim/ee/whitelabeling/metadata.ts +++ b/apps/sim/ee/whitelabeling/metadata.ts @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBaseUrl, SITE_URL } from '@/lib/core/utils/urls' import { getBrandConfig } from '@/ee/whitelabeling/branding' /** @@ -150,7 +150,7 @@ export function generateStructuredData() { creator: { '@type': 'Organization', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, }, featureList: [ 'AI Workspace for Teams', diff --git a/apps/sim/lib/blog/seo.ts b/apps/sim/lib/blog/seo.ts index d7e7693158c..a3fbd3f520e 100644 --- a/apps/sim/lib/blog/seo.ts +++ b/apps/sim/lib/blog/seo.ts @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import type { BlogMeta } from '@/lib/blog/schema' +import { SITE_URL } from '@/lib/core/utils/urls' export function buildPostMetadata(post: BlogMeta): Metadata { const base = new URL(post.canonical) @@ -85,10 +86,10 @@ export function buildArticleJsonLd(post: BlogMeta) { publisher: { '@type': 'Organization', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, logo: { '@type': 'ImageObject', - url: 'https://sim.ai/logo/primary/medium.png', + url: `${SITE_URL}/logo/primary/medium.png`, }, }, mainEntityOfPage: { @@ -112,8 +113,8 @@ export function buildBreadcrumbJsonLd(post: BlogMeta) { return { '@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: 1, name: 'Home', item: SITE_URL }, + { '@type': 'ListItem', position: 2, name: 'Blog', item: `${SITE_URL}/blog` }, { '@type': 'ListItem', position: 3, name: post.title, item: post.canonical }, ], } @@ -150,22 +151,22 @@ export function buildCollectionPageJsonLd() { '@context': 'https://schema.org', '@type': 'CollectionPage', name: 'Sim Blog', - url: 'https://sim.ai/blog', + url: `${SITE_URL}/blog`, description: 'Announcements, insights, and guides for building AI agents.', publisher: { '@type': 'Organization', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, logo: { '@type': 'ImageObject', - url: 'https://sim.ai/logo/primary/medium.png', + url: `${SITE_URL}/logo/primary/medium.png`, }, }, inLanguage: 'en-US', isPartOf: { '@type': 'WebSite', name: 'Sim', - url: 'https://sim.ai', + url: SITE_URL, }, } } diff --git a/apps/sim/lib/core/utils/urls.ts b/apps/sim/lib/core/utils/urls.ts index 5be78eb1d7a..15712176a8d 100644 --- a/apps/sim/lib/core/utils/urls.ts +++ b/apps/sim/lib/core/utils/urls.ts @@ -1,6 +1,9 @@ import { getEnv } from '@/lib/core/config/env' import { isProd } from '@/lib/core/config/feature-flags' +/** Canonical base URL for the public-facing marketing site. No trailing slash. */ +export const SITE_URL = 'https://www.sim.ai' + function hasHttpProtocol(url: string): boolean { return /^https?:\/\//i.test(url) } diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 9cd085983f0..bf7e51ce5d4 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -149,6 +149,15 @@ const nextConfig: NextConfig = { ], async headers() { return [ + { + source: '/:all*(svg|jpg|jpeg|png|gif|ico|webp|avif|woff|woff2|ttf|eot)', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=86400, stale-while-revalidate=604800', + }, + ], + }, { source: '/.well-known/:path*', headers: [ @@ -386,12 +395,12 @@ const nextConfig: NextConfig = { redirects.push( { source: '/building/:path*', - destination: 'https://sim.ai/blog/:path*', + destination: 'https://www.sim.ai/blog/:path*', permanent: true, }, { source: '/studio/:path*', - destination: 'https://sim.ai/blog/:path*', + destination: 'https://www.sim.ai/blog/:path*', permanent: true, } ) diff --git a/apps/sim/public/blog/executor/cover.png b/apps/sim/public/blog/executor/cover.png index 5f9031fcbf7..0dfda81e3df 100644 Binary files a/apps/sim/public/blog/executor/cover.png and b/apps/sim/public/blog/executor/cover.png differ diff --git a/apps/sim/public/blog/mothership/cover.jpg b/apps/sim/public/blog/mothership/cover.jpg new file mode 100644 index 00000000000..64d8c63dc29 Binary files /dev/null and b/apps/sim/public/blog/mothership/cover.jpg differ diff --git a/apps/sim/public/blog/mothership/cover.png b/apps/sim/public/blog/mothership/cover.png deleted file mode 100644 index c023f635727..00000000000 Binary files a/apps/sim/public/blog/mothership/cover.png and /dev/null differ diff --git a/apps/sim/public/blog/openai-vs-n8n-vs-sim/workflow.png b/apps/sim/public/blog/openai-vs-n8n-vs-sim/workflow.png index e5ba786385a..c37b9a5d494 100644 Binary files a/apps/sim/public/blog/openai-vs-n8n-vs-sim/workflow.png and b/apps/sim/public/blog/openai-vs-n8n-vs-sim/workflow.png differ diff --git a/apps/sim/public/blog/series-a/cover.png b/apps/sim/public/blog/series-a/cover.png index 71aeb92ce84..0a9ce8b8e43 100644 Binary files a/apps/sim/public/blog/series-a/cover.png and b/apps/sim/public/blog/series-a/cover.png differ diff --git a/apps/sim/public/blog/v0-5/cover.png b/apps/sim/public/blog/v0-5/cover.png index 6ccf4ba8e36..fa6f3729cd3 100644 Binary files a/apps/sim/public/blog/v0-5/cover.png and b/apps/sim/public/blog/v0-5/cover.png differ diff --git a/apps/sim/public/landing/multiplayer-cover.png b/apps/sim/public/landing/multiplayer-cover.png index ae54fbac62e..76f6eaf16e7 100644 Binary files a/apps/sim/public/landing/multiplayer-cover.png and b/apps/sim/public/landing/multiplayer-cover.png differ