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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/app/[lang]/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export async function generateMetadata(props: {
{
url: ogImageUrl,
width: 1200,
height: 630,
height: 675,
alt: data.title,
},
],
Expand Down
6 changes: 4 additions & 2 deletions apps/docs/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
SidebarItem,
SidebarSeparator,
} from '@/components/docs-layout/sidebar-components'
import { Footer } from '@/components/footer/footer'
import { Navbar } from '@/components/navbar/navbar'
import { SimLogoFull } from '@/components/ui/sim-logo'
import { SimWordmark } from '@/components/ui/sim-logo'
import { i18n } from '@/lib/i18n'
import { serializeJsonLd } from '@/lib/json-ld'
import { source } from '@/lib/source'
Expand Down Expand Up @@ -101,7 +102,7 @@ export default async function Layout({ children, params }: LayoutProps) {
<DocsLayout
tree={source.pageTree[lang]}
nav={{
title: <SimLogoFull className='h-[22px] w-auto' />,
title: <SimWordmark className='h-[18px]' />,
}}
sidebar={{
tabs: false,
Expand All @@ -121,6 +122,7 @@ export default async function Layout({ children, params }: LayoutProps) {
>
{children}
</DocsLayout>
<Footer />
<AskAI locale={lang} />
</RootProvider>
</body>
Expand Down
171 changes: 107 additions & 64 deletions apps/docs/app/api/og/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,39 @@ import type { NextRequest } from 'next/server'
export const runtime = 'edge'

const TITLE_FONT_SIZE = {
large: 64,
medium: 56,
small: 48,
large: 105,
medium: 91,
small: 81,
} as const
/** Average glyph width as a fraction of font size, for this weight/family — used to pack words into lines. */
const AVG_CHAR_WIDTH_EM = 0.46
const TITLE_BOX_WIDTH = 1020
const FONT_CACHE_REVALIDATE_SECONDS = 60 * 60 * 24 * 30
/** Measured off the reference cover template (`apps/sim/public/library/best-zapier-alternatives/cover.jpg`). */
const INK_COLOR = '#525252'
const OG_CONTAINER_STYLE = {
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '56px 64px',
background: '#121212',
fontFamily: 'Geist',
padding: '26px',
background: '#c3c3c3',
fontFamily: 'Season',
} satisfies CSSProperties
const OG_TITLE_STYLE = {
fontWeight: 500,
color: '#fafafa',
lineHeight: 1.2,
letterSpacing: '-0.02em',
} satisfies CSSProperties
const OG_FOOTER_STYLE = {
const OG_HEADER_STYLE = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
alignItems: 'flex-start',
width: '100%',
} satisfies CSSProperties
const OG_DOMAIN_STYLE = {
fontSize: 24,
fontWeight: 400,
color: '#71717a',
const OG_TITLE_STYLE = {
display: 'flex',
flexDirection: 'column',
fontWeight: 600,
color: INK_COLOR,
lineHeight: 1.15,
width: `${TITLE_BOX_WIDTH}px`,
} satisfies CSSProperties

function getTitleFontSize(title: string): number {
Expand All @@ -52,90 +54,131 @@ function getTitleStyle(title: string): CSSProperties {
}

/**
* Loads a Google Font dynamically by fetching the CSS and extracting the font URL.
* Greedily packs words into lines that fit `TITLE_BOX_WIDTH` at `fontSize`,
* then joins each line with U+00A0 instead of a plain space. Satori
* (`next/og`'s renderer) has a text-measurement bug where the first plain
* space (U+0020) in a text node renders at roughly double width — a
* non-breaking space measures correctly and reads identically at this size,
* so it sidesteps the bug instead of fighting Satori's own line-wrapping
* (which is also disabled here — lines are pre-split, not auto-wrapped).
*/
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 cssResponse = await fetch(url, {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})
function wrapTitleLines(title: string, fontSize: number): string[] {
const maxCharsPerLine = Math.floor(TITLE_BOX_WIDTH / (fontSize * AVG_CHAR_WIDTH_EM))
const words = title.split(' ')
const lines: string[] = []
let current = ''

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], {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})
if (response.ok) {
return await response.arrayBuffer()
for (const word of words) {
const candidate = current ? `${current} ${word}` : word
if (candidate.length > maxCharsPerLine && current) {
lines.push(current)
current = word
} else {
current = candidate
}
}
if (current) lines.push(current)

throw new Error('Failed to load font data')
return lines.map((line) => line.replace(/ /g, ' '))
Comment thread
waleedlatif1 marked this conversation as resolved.
}

/**
* Sim logo with icon and "Sim" text for OG image.
* Loads a static (600/semibold) TTF instance of the site's own Season Sans
* font — the platform's real brand/body font, also used by the library/blog
* cover template this OG image matches. Instantiated from the variable font
* at `apps/docs/app/fonts/SeasonSansUprightsVF.woff2` (`fonttools
* varLib.instancer wght=600`, then flavor-stripped to plain TTF) rather than
* loading the variable WOFF2 directly: Satori (`next/og`'s renderer) can't
* parse variable fonts without excessive memory use, and can't parse WOFF2
* at all ("Unsupported OpenType signature wOF2") — it needs an uncompressed
* TTF/OTF. Fetched over HTTP since the edge runtime has no filesystem access
* — served from `/static/fonts/` (not `/fonts/`) so it isn't intercepted by
* the site's i18n proxy (`proxy.ts`), whose matcher excludes `static` but
* not `fonts`.
*/
function SimLogoFull() {
async function loadSeasonFont(baseUrl: string): Promise<ArrayBuffer> {
const response = await fetch(new URL('/static/fonts/SeasonSans-600-static.ttf', baseUrl), {
next: { revalidate: FONT_CACHE_REVALIDATE_SECONDS },
})

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

return await response.arrayBuffer()
}

/** "sim" wordmark, no icon — same brandbook workmark geometry as the docs navbar/landing OG cards. */
function SimWordmark() {
return (
<svg height='34' viewBox='720 440 1020 320' fill='none'>
{/* Green icon - top left shape with cutout */}
<svg width='118' height='57' viewBox='0 0 800 386' fill='none'>
<path
d='M0 293.75h53.4128c0 14.748 5.3413 26.506 16.0239 35.275 10.6826 8.37 25.1238 12.555 43.3233 12.555 19.783 0 35.016-3.786 45.698-11.36 10.683-7.971 16.024-18.534 16.024-31.687 0-9.566-2.967-17.538-8.902-23.915-5.539-6.378-15.826-11.559-30.861-15.545l-51.0389-11.958c-25.7173-6.377-44.9063-16.142-57.5672-29.296-12.2651-13.153-18.39771-30.491-18.39771-52.015 0-17.936 4.55001-33.481 13.64991-46.635 9.4957-13.153 22.3543-23.3169 38.576-30.4914 16.6173-7.1745 35.6086-10.7619 56.9739-10.7619 21.365 0 39.763 3.7866 55.193 11.3598 15.826 7.5731 28.091 18.1355 36.796 31.6875 9.1 13.552 13.847 29.695 14.243 48.428h-53.413c-.395-15.146-5.341-26.904-14.837-35.275-9.495-8.37-22.75-12.555-39.763-12.555-17.4083 0-30.8604 3.786-40.356 11.36-9.4956 7.573-14.2434 17.936-14.2434 31.089 0 19.531 14.2434 32.884 42.7304 40.058l51.039 12.556c24.53 5.58 42.928 14.747 55.193 27.502 12.265 12.356 18.398 29.296 18.398 50.82 0 18.335-4.946 34.477-14.837 48.428-9.891 13.552-23.541 24.114-40.95 31.687-17.013 7.175-37.191 10.762-60.534 10.762-34.0265 0-61.1285-8.37-81.3067-25.111-20.1782-16.74-30.2673-39.061-30.2673-66.962z'
fill={INK_COLOR}
/>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M875.791 577.171C875.791 581.922 873.911 586.483 870.576 589.842L870.098 590.323C866.764 593.692 862.234 595.575 857.517 595.575H750.806C740.978 595.575 733 603.6 733 613.498V728.902C733 738.799 740.978 746.826 750.806 746.826H865.382C875.209 746.826 883.177 738.799 883.177 728.902V620.853C883.177 616.448 884.912 612.222 888.008 609.104C891.093 605.997 895.29 604.249 899.664 604.249H1008.16C1017.99 604.249 1025.96 596.224 1025.96 586.327V470.923C1025.96 461.025 1017.99 453 1008.16 453H893.586C883.759 453 875.791 461.025 875.791 470.923V577.171ZM910.562 477.566H991.178C996.922 477.566 1001.57 482.254 1001.57 488.029V569.22C1001.57 574.995 996.922 579.683 991.178 579.683H910.562C904.828 579.683 900.173 574.995 900.173 569.22V488.029C900.173 482.254 904.828 477.566 910.562 477.566Z'
fill='#33C482'
d='m267.175 385.826v-292.3631c22.244 8.1331 32.053 8.1331 55.787 0v292.3631zm27.3-311.6891c-9.891 0-18.596-3.5872-26.113-10.7618-7.122-7.5731-10.683-16.342-10.683-26.3067 0-10.3632 3.561-19.132 10.683-26.3066 7.517-7.17453 16.222-10.7618 26.113-10.7618 10.287 0 18.991 3.58727 26.113 10.7618 7.122 7.1746 10.682 15.9434 10.682 26.3066 0 9.9647-3.56 18.7336-10.682 26.3067-7.122 7.1746-15.826 10.7618-26.113 10.7618z'
fill={INK_COLOR}
/>
{/* Green icon - bottom right square */}
<path
d='M1008.3 624.59H923.113C912.786 624.59 904.414 633.022 904.414 643.423V728.171C904.414 738.572 912.786 747.004 923.113 747.004H1008.3C1018.63 747.004 1027 738.572 1027 728.171V643.423C1027 633.022 1018.63 624.59 1008.3 624.59Z'
fill='#33C482'
d='m421.362 385.823h-55.786v-292.3624h49.852v49.3294c5.934-16.342 17.408-30.197 33.234-40.959 16.222-11.1605 35.807-16.7407 58.754-16.7407 25.718 0 47.083 6.9752 64.096 20.9257 17.013 13.951 28.091 32.485 33.234 55.603h-10.089c3.957-23.118 14.837-41.652 32.642-55.603 17.804-13.9505 39.762-20.9257 65.875-20.9257 33.235 0 59.348 9.7653 78.339 29.2957 18.991 19.531 28.487 46.236 28.487 80.116v191.321h-54.6v-177.57c0-23.118-5.934-40.855-17.804-53.211-11.474-12.755-27.102-19.132-46.885-19.132-13.847 0-26.113 3.189-36.795 9.566-10.287 5.979-18.398 14.748-24.333 26.307-5.934 11.559-8.902 25.111-8.902 40.655v173.385h-55.193v-178.168c0-23.118-5.737-40.655-17.211-52.613-11.474-12.356-27.102-18.534-46.885-18.534-13.847 0-26.112 3.189-36.795 9.566-10.287 5.979-18.398 14.748-24.333 26.307-5.934 11.16-8.902 24.513-8.902 40.057z'
fill={INK_COLOR}
/>
{/* "Sim" text - white for dark background */}
</svg>
)
}

/** Diagonal "open" arrow, top-right — matches the library/blog cover template. */
function CornerArrow() {
return (
<svg width='58' height='58' viewBox='0 0 24 24' fill='none'>
<path
d='M1210.54 515.657C1226.65 515.657 1240.59 518.51 1252.31 524.257H1252.31C1264.3 529.995 1273.63 538.014 1280.26 548.319H1280.26C1287.19 558.635 1290.78 570.899 1291.08 585.068L1291.1 586.089H1249.11L1249.09 585.115C1248.8 574.003 1245.18 565.493 1238.32 559.451C1231.45 553.399 1221.79 550.308 1209.21 550.308C1196.3 550.308 1186.48 553.113 1179.61 558.588C1172.76 564.046 1169.33 571.499 1169.33 581.063C1169.33 588.092 1171.88 593.978 1177.01 598.783C1182.17 603.618 1189.99 607.399 1200.56 610.061H1200.56L1238.77 619.451C1257.24 623.65 1271.21 630.571 1280.57 640.293L1281.01 640.739C1290.13 650.171 1294.64 662.97 1294.64 679.016C1294.64 692.923 1290.88 705.205 1283.34 715.822L1283.33 715.834C1275.81 726.134 1265.44 734.14 1252.26 739.866L1252.25 739.871C1239.36 745.302 1224.12 748 1206.54 748C1180.9 748 1160.36 741.696 1145.02 728.984C1129.67 716.258 1122 699.269 1122 678.121V677.121H1163.99V678.121C1163.99 688.869 1167.87 697.367 1175.61 703.722L1176.34 704.284C1184.04 709.997 1194.37 712.902 1207.43 712.902C1222.13 712.902 1233.3 710.087 1241.07 704.588C1248.8 698.812 1252.64 691.21 1252.64 681.699C1252.64 674.769 1250.5 669.057 1246.25 664.49L1246.23 664.478L1246.22 664.464C1242.28 659.929 1234.83 656.119 1223.64 653.152L1185.43 644.208L1185.42 644.204C1166.05 639.407 1151.49 632.035 1141.83 622.012L1141.83 622.006L1141.82 622C1132.43 611.94 1127.78 598.707 1127.78 582.405C1127.78 568.81 1131.23 556.976 1138.17 546.949L1138.18 546.941L1138.19 546.933C1145.41 536.936 1155.18 529.225 1167.48 523.793L1167.48 523.79C1180.07 518.36 1194.43 515.657 1210.54 515.657ZM1323.39 521.979C1331.68 525.008 1337.55 526.482 1343.51 526.482C1349.48 526.482 1355.64 525.005 1364.49 521.973L1365.82 521.52V742.633H1322.05V521.489L1323.39 521.979ZM1642.01 515.657C1667.11 515.657 1686.94 523.031 1701.39 537.876C1715.83 552.716 1723 572.968 1723 598.507V742.633H1680.12V608.794C1680.12 591.666 1675.72 578.681 1667.07 569.681L1667.06 569.669L1667.04 569.656C1658.67 560.359 1647.26 555.675 1632.68 555.675C1622.47 555.675 1613.47 558.022 1605.64 562.69L1605.63 562.696C1598.11 567.064 1592.17 573.475 1587.8 581.968C1583.44 590.448 1581.25 600.424 1581.25 611.925V742.633H1537.92V608.347C1537.92 591.208 1533.67 578.376 1525.31 569.68L1525.31 569.674L1525.3 569.668C1516.93 560.664 1505.52 556.122 1490.93 556.122C1480.72 556.122 1471.72 558.469 1463.89 563.138L1463.88 563.144C1456.36 567.511 1450.41 573.922 1446.05 582.415L1446.05 582.422L1446.04 582.428C1441.69 590.602 1439.5 600.423 1439.5 611.925V742.633H1395.72V521.919H1435.05V554.803C1439.92 544.379 1447.91 535.465 1458.37 528.356C1470.71 519.875 1485.58 515.657 1502.93 515.657C1522.37 515.657 1538.61 520.931 1551.55 531.538C1560.38 538.771 1567.1 547.628 1571.72 558.091C1576.05 547.619 1582.83 538.757 1592.07 531.524C1605.61 520.93 1622.28 515.657 1642.01 515.657ZM1343.49 452C1351.45 452 1358.23 454.786 1363.75 460.346C1369.27 465.905 1372.04 472.721 1372.04 480.73C1372.04 488.452 1369.27 495.254 1363.77 501.096L1363.76 501.105L1363.75 501.115C1358.23 506.675 1351.45 509.461 1343.49 509.461C1335.81 509.461 1329.05 506.669 1323.25 501.134L1323.23 501.115L1323.21 501.096C1317.71 495.254 1314.94 488.452 1314.94 480.73C1314.94 472.721 1317.7 465.905 1323.23 460.346L1323.24 460.337L1323.25 460.327C1329.05 454.792 1335.81 452 1343.49 452Z'
fill='#fafafa'
d='M2 22 22 2M22 2H12M22 2V12'
stroke={INK_COLOR}
strokeWidth={3.6}
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
)
}

/**
* Generates dynamic Open Graph images for documentation pages.
* Style matches Cursor docs: dark background, title at top, logo bottom-left, domain bottom-right.
* Generates dynamic Open Graph images for documentation pages. Matches the
* site's library/blog cover template: light gray background, "sim" wordmark
* top-left, an open/diagonal arrow top-right, and the page title large and
* bold at the bottom-left.
*/
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') || 'Documentation'

const allText = `${title}docs.sim.ai`
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)
const fontData = await loadSeasonFont(request.url)
const fontSize = getTitleFontSize(title)
const titleLines = wrapTitleLines(title, fontSize)

return new ImageResponse(
<div style={OG_CONTAINER_STYLE}>
{/* Title at top */}
<span style={getTitleStyle(title)}>{title}</span>
<div style={OG_HEADER_STYLE}>
<SimWordmark />
<CornerArrow />
</div>

{/* Footer: icon left, domain right */}
<div style={OG_FOOTER_STYLE}>
<SimLogoFull />
<span style={OG_DOMAIN_STYLE}>docs.sim.ai</span>
<div style={getTitleStyle(title)}>
{titleLines.map((line, index) => (
<span key={index}>{line}</span>
))}
</div>
</div>,
{
width: 1200,
height: 630,
height: 675,
fonts: [
{
name: 'Geist',
name: 'Season',
data: fontData,
style: 'normal',
weight: 600,
},
],
}
Expand Down
14 changes: 11 additions & 3 deletions apps/docs/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,20 @@ aside#nd-sidebar [data-radix-scroll-area-viewport] {
min-height: var(--fd-docs-height) !important;
}

/* Sidebar divider line — fixed to viewport, full height from navbar to bottom */
/* Sidebar divider line — sticky within the docs layout box, so it ends where
the layout does instead of bleeding into (or past) the footer below it.
#nd-docs-layout is a CSS grid (see fumadocs' Container slot); a sticky
pseudo-element stays in normal flow, so without an explicit grid-area it
gets auto-placed into a real content cell and skews that cell's sizing.
Spanning the full grid keeps it purely decorative/overlaid instead. */
#nd-docs-layout::before {
content: "";
position: fixed;
display: block;
position: sticky;
grid-row: 1 / -1;
grid-column: 1 / -1;
top: 92px; /* below navbar */
bottom: 0;
height: calc(100dvh - 92px);
left: calc(var(--sidebar-offset) + var(--fd-sidebar-width));
width: 1px;
background-color: var(--surface-active);
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const metadata = {
{
url: `${DOCS_BASE_URL}/api/og?title=Sim%20Documentation`,
width: 1200,
height: 630,
height: 675,
alt: 'Sim Documentation',
},
],
Expand Down
Loading
Loading