Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 6 additions & 3 deletions apps/docs/app/[lang]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,12 @@ export async function generateMetadata(props: {
siteName: 'Sim Documentation',
type: 'article',
locale: OG_LOCALE_MAP[lang] ?? 'en_US',
alternateLocale: i18n.languages
.filter((l) => l !== lang)
.map((l) => OG_LOCALE_MAP[l] ?? 'en_US'),
alternateLocale: i18n.languages.reduce<string[]>((locales, l) => {
if (l !== lang) {
locales.push(OG_LOCALE_MAP[l] ?? 'en_US')
}
return locales
}, []),
images: [
{
url: ogImageUrl,
Expand Down
11 changes: 2 additions & 9 deletions apps/docs/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { Navbar } from '@/components/navbar/navbar'
import { SimLogoFull } from '@/components/ui/sim-logo'
import { i18n } from '@/lib/i18n'
import { serializeJsonLd } from '@/lib/json-ld'
import { source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
import '../global.css'
Expand Down Expand Up @@ -78,14 +79,6 @@ export default async function Layout({ children, params }: LayoutProps) {
},
},
inLanguage: lang,
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${DOCS_BASE_URL}/api/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
}

return (
Expand All @@ -97,7 +90,7 @@ export default async function Layout({ children, params }: LayoutProps) {
<head>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(structuredData) }}
/>
</head>
<body className='flex min-h-screen flex-col font-sans'>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/[lang]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function NotFound() {
return (
<DocsPage>
<div className='flex min-h-[70vh] flex-col items-center justify-center gap-4 text-center'>
<h1 className='bg-gradient-to-b from-[#47d991] to-[#33c482] bg-clip-text font-bold text-8xl text-transparent'>
<h1 className='bg-gradient-to-b from-[#47d991] to-[#33c482] bg-clip-text font-semibold text-8xl text-transparent'>
404
</h1>
<h2 className='font-semibold text-2xl text-foreground'>Page Not Found</h2>
Expand Down
96 changes: 53 additions & 43 deletions apps/docs/app/api/og/route.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CSSProperties } from 'react'
import { ImageResponse } from 'next/og'
import type { NextRequest } from 'next/server'

Expand All @@ -8,24 +9,69 @@ const TITLE_FONT_SIZE = {
medium: 56,
small: 48,
} as const
const FONT_CACHE_REVALIDATE_SECONDS = 60 * 60 * 24 * 30
const OG_CONTAINER_STYLE = {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '56px 64px',
background: '#121212',
fontFamily: 'Geist',
} satisfies CSSProperties
const OG_TITLE_STYLE = {
fontWeight: 500,
color: '#fafafa',
lineHeight: 1.2,
letterSpacing: '-0.02em',
} satisfies CSSProperties
const OG_FOOTER_STYLE = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
} satisfies CSSProperties
const OG_DOMAIN_STYLE = {
fontSize: 20,
fontWeight: 400,
color: '#71717a',
} satisfies CSSProperties

function getTitleFontSize(title: string): number {
if (title.length > 45) return TITLE_FONT_SIZE.small
if (title.length > 30) return TITLE_FONT_SIZE.medium
return TITLE_FONT_SIZE.large
}

function getTitleStyle(title: string): CSSProperties {
return {
...OG_TITLE_STYLE,
fontSize: getTitleFontSize(title),
}
}

/**
* Loads a Google Font dynamically by fetching the CSS and extracting the font URL.
*/
async function loadGoogleFont(font: string, weights: string, text: string): Promise<ArrayBuffer> {
const url = `https://fonts.googleapis.com/css2?family=${font}:wght@${weights}&text=${encodeURIComponent(text)}`
const css = await (await fetch(url)).text()
const cssResponse = await fetch(url, {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})

if (!cssResponse.ok) {
throw new Error(`Failed to load font CSS: ${cssResponse.status} ${cssResponse.statusText}`)
}

const css = await cssResponse.text()
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)

if (resource) {
const response = await fetch(resource[1])
if (response.status === 200) {
const response = await fetch(resource[1], {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})
if (response.ok) {
return await response.arrayBuffer()
}
}
Expand Down Expand Up @@ -72,50 +118,14 @@ export async function GET(request: NextRequest) {
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)

return new ImageResponse(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '56px 64px',
background: '#121212', // Dark mode background matching docs (hsla 0, 0%, 7%)
fontFamily: 'Geist',
}}
>
<div style={OG_CONTAINER_STYLE}>
{/* Title at top */}
<span
style={{
fontSize: getTitleFontSize(title),
fontWeight: 500,
color: '#fafafa', // Light text matching docs
lineHeight: 1.2,
letterSpacing: '-0.02em',
}}
>
{title}
</span>
<span style={getTitleStyle(title)}>{title}</span>

{/* Footer: icon left, domain right */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
}}
>
<div style={OG_FOOTER_STYLE}>
<SimLogoFull />
<span
style={{
fontSize: 20,
fontWeight: 400,
color: '#71717a',
}}
>
docs.sim.ai
</span>
<span style={OG_DOMAIN_STYLE}>docs.sim.ai</span>
</div>
</div>,
{
Expand Down
92 changes: 60 additions & 32 deletions apps/docs/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,36 @@ import { generateSearchEmbedding } from '@/lib/embeddings'
export const runtime = 'nodejs'
export const revalidate = 0

const DEFAULT_SEARCH_LIMIT = 10
const MAX_SEARCH_LIMIT = 20

function getSearchLimit(value: unknown): number {
const limit = Number.parseInt(String(value ?? DEFAULT_SEARCH_LIMIT), 10)

if (!Number.isFinite(limit) || limit <= 0) {
return DEFAULT_SEARCH_LIMIT
}

return Math.min(limit, MAX_SEARCH_LIMIT)
}

function getSearchParams(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
return {
query: searchParams.get('query') || searchParams.get('q') || '',
locale: searchParams.get('locale') || 'en',
limit: getSearchLimit(searchParams.get('limit')),
}
}

/**
* Hybrid search API endpoint
* - English: Vector embeddings + keyword search
* - Other languages: Keyword search only
*/
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query') || searchParams.get('q') || ''
const locale = searchParams.get('locale') || 'en'
const limit = Number.parseInt(searchParams.get('limit') || '10', 10)
const { query, locale, limit } = getSearchParams(request)

if (!query || query.trim().length === 0) {
return NextResponse.json([])
Expand Down Expand Up @@ -94,6 +113,10 @@ export async function GET(request: NextRequest) {
const keywordRankMap = new Map<string, number>()
keywordResults.forEach((r, idx) => keywordRankMap.set(r.chunkId, idx + 1))

const resultByChunkId = new Map<string, (typeof vectorResults)[number]>()
keywordResults.forEach((result) => resultByChunkId.set(result.chunkId, result))
vectorResults.forEach((result) => resultByChunkId.set(result.chunkId, result))

const allChunkIds = new Set([
...vectorResults.map((r) => r.chunkId),
...keywordResults.map((r) => r.chunkId),
Expand All @@ -109,9 +132,7 @@ export async function GET(request: NextRequest) {

const rrfScore = 1 / (k + vectorRank) + 1 / (k + keywordRank)

const result =
vectorResults.find((r) => r.chunkId === chunkId) ||
keywordResults.find((r) => r.chunkId === chunkId)
const result = resultByChunkId.get(chunkId)

if (result) {
scoredResults.push({ ...result, rrfScore })
Expand Down Expand Up @@ -167,31 +188,38 @@ export async function GET(request: NextRequest) {
const pathParts = result.sourceDocument
.replace('.mdx', '')
.split('/')
.filter((part) => part !== 'index' && !knownLocales.includes(part))
.map((part) => {
return part
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const acronyms = [
'api',
'mcp',
'sdk',
'url',
'http',
'json',
'xml',
'html',
'css',
'ai',
]
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase()
}
return word.charAt(0).toUpperCase() + word.slice(1)
})
.join(' ')
})
.reduce<string[]>((parts, part) => {
if (part === 'index' || knownLocales.includes(part)) {
return parts
}

parts.push(
part
.replace(/_/g, ' ')
.split(' ')
.map((word) => {
const acronyms = [
'api',
'mcp',
'sdk',
'url',
'http',
'json',
'xml',
'html',
'css',
'ai',
]
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase()
}
return word.charAt(0).toUpperCase() + word.slice(1)
})
.join(' ')
)

return parts
}, [])

return {
id: result.chunkId,
Expand Down
1 change: 0 additions & 1 deletion apps/docs/app/robots.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Disallow: /api/internal/
Disallow: /_next/static/
Disallow: /admin/
Allow: /
Allow: /api/search
Allow: /llms.txt
Allow: /llms-full.txt
Allow: /llms.mdx/
Expand Down
6 changes: 3 additions & 3 deletions apps/docs/components/docs-layout/page-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function PageFooter({ previous, next }: PageFooterProps) {
Previous
</span>
<span className='flex items-center gap-1.5 font-[470] text-[rgba(0,0,0,0.7)] text-sm transition-colors group-hover:text-[rgba(0,0,0,0.88)] dark:text-[rgba(255,255,255,0.7)] dark:group-hover:text-[rgba(255,255,255,0.92)]'>
<ChevronLeft className='h-3.5 w-3.5 shrink-0' />
<ChevronLeft className='size-3.5 shrink-0' />
{previous.name}
</span>
</Link>
Expand All @@ -68,7 +68,7 @@ export function PageFooter({ previous, next }: PageFooterProps) {
</span>
<span className='flex items-center gap-1.5 font-[470] text-[rgba(0,0,0,0.7)] text-sm transition-colors group-hover:text-[rgba(0,0,0,0.88)] dark:text-[rgba(255,255,255,0.7)] dark:group-hover:text-[rgba(255,255,255,0.92)]'>
{next.name}
<ChevronRight className='h-3.5 w-3.5 shrink-0' />
<ChevronRight className='size-3.5 shrink-0' />
</span>
</Link>
) : (
Expand All @@ -90,7 +90,7 @@ export function PageFooter({ previous, next }: PageFooterProps) {
>
<svg
viewBox='0 0 24 24'
className='h-5 w-5 fill-gray-400 transition-colors hover:fill-gray-500 dark:fill-gray-500 dark:hover:fill-gray-400'
className='size-5 fill-neutral-400 transition-colors hover:fill-neutral-500 dark:fill-neutral-500 dark:hover:fill-neutral-400'
>
<path d={link.icon} />
</svg>
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/components/docs-layout/page-navigation-arrows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function PageNavigationArrows({ previous, next }: PageNavigationArrowsPro
aria-label='Previous page'
title='Previous page'
>
<ChevronLeft className='h-4 w-4' />
<ChevronLeft className='size-4' />
</Link>
)}
{next && (
Expand All @@ -34,7 +34,7 @@ export function PageNavigationArrows({ previous, next }: PageNavigationArrowsPro
aria-label='Next page'
title='Next page'
>
<ChevronRight className='h-4 w-4' />
<ChevronRight className='size-4' />
</Link>
)}
</div>
Expand Down
Loading
Loading