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
@@ -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}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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.',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 765 KiB After Width: | Height: | Size: 19 KiB |