fix(seo): correct canonical URLs, compress oversized images, add cache headers (#4168)

* fix(seo): correct canonical URLs, compress oversized images, add cache headers

- Replace all hardcoded https://sim.ai with https://www.sim.ai via SITE_URL constant
- Migrate models, integrations, and homepage metadata from getBaseUrl() to SITE_URL
- Compress 6 blog/landing images from 2.6MB to 300KB total
- Convert mothership cover from PNG to JPEG (1.1MB → 99KB)
- Add Cache-Control headers for static assets (1d max-age, 7d stale-while-revalidate)
- Add SEO regression test scanning all public pages for canonical URL violations

* fix(seo): replace hardcoded URLs with SITE_URL, broaden test detection

- Replace hardcoded https://www.sim.ai with SITE_URL in academy, changelog.xml, and whitelabeling
- Broaden getBaseUrl() detection in SEO test to match any variable name assignment
- Add ee/whitelabeling/metadata.ts to SEO test scan scope
This commit is contained in:
Emir Karabeg
2026-04-14 16:42:32 -07:00
committed by GitHub
parent 751eeaccd4
commit cbf0a139ed
41 changed files with 243 additions and 93 deletions
@@ -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}`,
},
],
},
+4 -3
View File
@@ -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 (
+3 -2
View File
@@ -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',
+2 -1
View File
@@ -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()
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
${posts
+6 -5
View File
@@ -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` },
],
}
@@ -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',
@@ -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]))
@@ -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',
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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' }],
@@ -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({
<div className='flex flex-wrap gap-2'>
<a
href='https://sim.ai'
href='/'
className='inline-flex h-[32px] items-center gap-2 rounded-[5px] border border-white bg-white px-2.5 font-season text-black text-sm transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Build with this model
@@ -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) => ({
+2 -2
View File
@@ -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',
+2 -2
View File
@@ -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 = [
{
+2 -1
View File
@@ -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.',
+127
View File
@@ -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)
})
})
+2 -1
View File
@@ -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.',
+2 -1
View File
@@ -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() {
<rss version="2.0">
<channel>
<title>Sim Changelog</title>
<link>https://sim.ai/changelog</link>
<link>${SITE_URL}/changelog</link>
<description>Latest changes, fixes and updates in Sim.</description>
<language>en-us</language>
${items}
+2
View File
@@ -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.',
+6 -8
View File
@@ -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: {
+1 -1
View File
@@ -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
---
+1 -1
View File
@@ -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
---
+1 -1
View File
@@ -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
---
+1 -1
View File
@@ -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
---
+2 -2
View File
@@ -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
---
+1 -1
View File
@@ -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
---
@@ -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
---
+1 -1
View File
@@ -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
---
+1 -1
View File
@@ -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
---
+2 -2
View File
@@ -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',
+9 -8
View File
@@ -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,
},
}
}
+3
View File
@@ -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)
}
+11 -2
View File
@@ -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,
}
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 KiB

After

Width:  |  Height:  |  Size: 19 KiB