diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 810e530..7e8ad11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,6 +235,9 @@ importers: specifier: ^6.0.3 version: 6.0.3 devDependencies: + '@resvg/resvg-js': + specifier: ^2.6.2 + version: 2.6.2 '@types/react': specifier: ^19.2.16 version: 19.2.16 @@ -1354,6 +1357,86 @@ packages: react: '>= 16.8' react-dom: '>= 16.8' + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@rolldown/binding-android-arm64@1.0.3': resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5492,6 +5575,57 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@rolldown/binding-android-arm64@1.0.3': optional: true diff --git a/sites/web/package.json b/sites/web/package.json index fcc771f..ae4dd81 100644 --- a/sites/web/package.json +++ b/sites/web/package.json @@ -6,7 +6,8 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "check:types": "astro check" + "check:types": "astro check", + "og": "node scripts/generate-og.mjs" }, "dependencies": { "@astrojs/check": "^0.9.4", @@ -27,6 +28,7 @@ "typescript": "^6.0.3" }, "devDependencies": { + "@resvg/resvg-js": "^2.6.2", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3" } diff --git a/sites/web/public/og.png b/sites/web/public/og.png new file mode 100644 index 0000000..08856fa Binary files /dev/null and b/sites/web/public/og.png differ diff --git a/sites/web/public/og.svg b/sites/web/public/og.svg index 8731460..552452a 100644 --- a/sites/web/public/og.svg +++ b/sites/web/public/og.svg @@ -15,24 +15,24 @@ - react-call() + react-call() - + Your component - can await. + can await. - + createCallable() · 1 KB · no deps · SSR · React Native - + react-call.desko.dev diff --git a/sites/web/scripts/generate-og.mjs b/sites/web/scripts/generate-og.mjs new file mode 100644 index 0000000..aa1e50a --- /dev/null +++ b/sites/web/scripts/generate-og.mjs @@ -0,0 +1,44 @@ +// Rasterizes public/og.svg into public/og.png. +// +// Why this exists: SVG is not a valid Open Graph image format. Reddit, +// X/Twitter, Facebook, LinkedIn, Slack, Discord and iMessage all ignore an +// `og:image` that points at an SVG and fall back to a generic placeholder, so +// shared links show no preview. og.svg stays the editable source of truth and +// this script bakes it into the PNG that the meta tags actually reference. +// +// Fonts are vendored as TTFs (scripts/og-fonts) and loaded explicitly so the +// render is deterministic and on-brand (Geist + Fira Code) regardless of which +// fonts the build machine happens to have installed — resvg renders no text at +// all when it cannot resolve a font. +// +// Run after editing og.svg: pnpm --filter web og + +import { readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { Resvg } from '@resvg/resvg-js' + +const here = dirname(fileURLToPath(import.meta.url)) +const publicDir = join(here, '..', 'public') +const fontsDir = join(here, 'og-fonts') + +const svg = readFileSync(join(publicDir, 'og.svg'), 'utf8') +const fontFiles = readdirSync(fontsDir) + .filter((f) => f.endsWith('.ttf')) + .map((f) => join(fontsDir, f)) + +const resvg = new Resvg(svg, { + // The SVG already declares width/height 1200x630; honor it 1:1. + fitTo: { mode: 'width', value: 1200 }, + font: { + fontFiles, + loadSystemFonts: false, + defaultFontFamily: 'Geist', + }, +}) + +const png = resvg.render().asPng() +const out = join(publicDir, 'og.png') +writeFileSync(out, png) + +console.log(`Wrote ${out} (${png.length} bytes)`) diff --git a/sites/web/scripts/og-fonts/FiraCode-Regular.ttf b/sites/web/scripts/og-fonts/FiraCode-Regular.ttf new file mode 100644 index 0000000..8ff1c40 Binary files /dev/null and b/sites/web/scripts/og-fonts/FiraCode-Regular.ttf differ diff --git a/sites/web/scripts/og-fonts/Geist-Medium.ttf b/sites/web/scripts/og-fonts/Geist-Medium.ttf new file mode 100644 index 0000000..8b295ed Binary files /dev/null and b/sites/web/scripts/og-fonts/Geist-Medium.ttf differ diff --git a/sites/web/scripts/og-fonts/Geist-Regular.ttf b/sites/web/scripts/og-fonts/Geist-Regular.ttf new file mode 100644 index 0000000..67e6992 Binary files /dev/null and b/sites/web/scripts/og-fonts/Geist-Regular.ttf differ diff --git a/sites/web/src/layouts/Base.astro b/sites/web/src/layouts/Base.astro index 6e8f6ee..4010047 100644 --- a/sites/web/src/layouts/Base.astro +++ b/sites/web/src/layouts/Base.astro @@ -14,7 +14,10 @@ interface Props { const { title = 'react-call — your component can await', description = 'createCallable turns any React component into something you can await. Dialogs, toasts, pickers, menus — any UI that conceptually returns a value to its caller.', - image = '/og.svg', + // Must be a raster format: Reddit, X, Facebook, LinkedIn, Slack, Discord and + // iMessage all ignore an SVG og:image. og.png is generated from og.svg via + // `pnpm --filter web og` — see sites/web/scripts/generate-og.mjs. + image = '/og.png', type = 'website', noindex = false, } = Astro.props @@ -57,6 +60,10 @@ const imageUrl = new URL(image, site).href + + + + @@ -64,6 +71,7 @@ const imageUrl = new URL(image, site).href +