ray.so Code Image Generation
Generate code snippet images by constructing a ray.so URL, navigating in Chrome via Playwriter,
pressing Cmd+C to copy the rendered image to clipboard, and saving the clipboard to a file.
Always use the vercel theme unless the user specifies a different one.
URL structure
All settings live in the hash fragment (#key=value&key2=value2), not query params.
Code is Base64url-encoded using Node.js Buffer.from(code).toString('base64url').
https://ray.so/#code=BASE64URL_CODE&theme=vercel&darkMode=true&background=true&padding=64&language=typescript&title=example.ts
Parameters
| Parameter |
Default |
Values |
code |
(sample) |
Buffer.from(code).toString('base64url') |
theme |
candy |
vercel, supabase, tailwind, openai, mintlify, prisma, clerk, elevenlabs, resend, triggerdev, nuxt, browserbase, cloudflare, gemini, stripe, bitmap, noir, ice, sand, forest, mono, breeze, candy, crimson, falcon, meadow, midnight, raindrop, sunset, rabbit, firecrawl, aws, auth0 |
darkMode |
true |
true, false |
background |
true |
true, false |
padding |
64 |
16, 32, 64, 128 |
language |
auto-detect |
javascript, typescript, tsx, jsx, python, go, rust, swift, shell, css, html, json, sql, ruby, java, kotlin, cpp, csharp, dart, elixir, graphql, haskell, lua, markdown, php, scala, toml, yaml, zig, diff, dockerfile, plaintext, and more |
title |
"" |
Filename shown in the title bar |
subtitle |
"" |
Subtitle text |
width |
auto |
Pixel width (number) |
lineNumbers |
undefined |
true, false |
highlightedLines |
"" |
Comma-separated line numbers |
Workflow: generate and save a code image
Single Playwriter call. Use the script below exactly as-is, only changing the
code, language, title, theme, and output path variables. Do not restructure,
reorder, or simplify the script; the clipboard interceptor must be injected before
Cmd+C is pressed, and the poll loop must run after.
playwriter -s <sessionId> --timeout 30000 -e "$(cat <<'PYEOF'
const code = `export function hello() {
return "world"
}`
const encoded = Buffer.from(code).toString('base64url')
const params = new URLSearchParams()
params.set('code', encoded)
params.set('theme', 'vercel')
params.set('darkMode', 'true')
params.set('background', 'true')
params.set('padding', '64')
params.set('language', 'typescript')
params.set('title', 'hello.ts')
// Navigate (always fresh page)
state.page = await context.newPage()
await state.page.goto('https://ray.so/#' + params.toString(), { waitUntil: 'domcontentloaded' })
await state.page.waitForSelector('#frame', { timeout: 10000 })
// Intercept clipboard write to capture the PNG blob
await state.page.evaluate(() => {
window.__clipboardBlob = null
const origWrite = navigator.clipboard.write.bind(navigator.clipboard)
navigator.clipboard.write = async (items) => {
for (const item of items) {
if (item.types.includes('image/png')) {
window.__clipboardBlob = await item.getType('image/png')
}
}
return origWrite(items)
}
})
// Press Cmd+C and poll until the blob is captured (no fixed sleep)
await state.page.keyboard.press('Meta+c')
let attempts = 0
while (attempts < 50) {
const hasBlob = await state.page.evaluate(() => !!window.__clipboardBlob)
if (hasBlob) break
await state.page.waitForTimeout(100)
attempts++
}
if (attempts >= 50) throw new Error('Clipboard blob not captured after 5s')
// Read blob as base64 and save via Node.js fs
const base64 = await state.page.evaluate(async () => {
const blob = window.__clipboardBlob
const buf = await blob.arrayBuffer()
const bytes = new Uint8Array(buf)
let binary = ''
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])
return btoa(binary)
})
const fs = require('node:fs')
const buffer = Buffer.from(base64, 'base64')
fs.writeFileSync('/absolute/path/to/output.png', buffer)
console.log('Saved', buffer.length, 'bytes')
// Close the page
await state.page.close()
PYEOF
)"
The clipboard poll checks every 100ms until the PNG blob is captured, with a 5s timeout.
After saving, verify the output image with the read-media tool.
Agent rules
- Always use
vercel theme unless the user asks for something else
- Always use
darkMode=true, background=true, padding=64 by default
- Set
language explicitly when known; do not rely on auto-detect
- Set
title to the filename when available (e.g. greet.ts)
- Use absolute paths for the output PNG
- Do not use
js-base64 npm package in Playwriter; use Buffer.from(code).toString('base64url') instead (Playwriter sandbox only allows Node.js built-ins)
- Do not inspect the page between navigate and
Cmd+C; the URL params are deterministic
- After saving, verify the image with
read-media tool to confirm it rendered correctly
1---2name: ray-so3description: Generate beautiful code snippet images using ray.so and Playwriter. Builds a URL with code, theme, and settings encoded in the hash fragment, navigates to it in Chrome, and exports the image via Cmd+C clipboard copy. Use this skill when the user asks to create a code image, code screenshot, or code snippet image.4---56# ray.so Code Image Generation78Generate code snippet images by constructing a ray.so URL, navigating in Chrome via Playwriter,9pressing `Cmd+C` to copy the rendered image to clipboard, and saving the clipboard to a file.1011Always use the **vercel** theme unless the user specifies a different one.1213## URL structure1415All settings live in the **hash fragment** (`#key=value&key2=value2`), not query params.16Code is Base64url-encoded using Node.js `Buffer.from(code).toString('base64url')`.1718```19https://ray.so/#code=BASE64URL_CODE&theme=vercel&darkMode=true&background=true&padding=64&language=typescript&title=example.ts20```2122## Parameters2324| Parameter | Default | Values |25|-----------|---------|--------|26| `code` | (sample) | `Buffer.from(code).toString('base64url')` |27| `theme` | `candy` | `vercel`, `supabase`, `tailwind`, `openai`, `mintlify`, `prisma`, `clerk`, `elevenlabs`, `resend`, `triggerdev`, `nuxt`, `browserbase`, `cloudflare`, `gemini`, `stripe`, `bitmap`, `noir`, `ice`, `sand`, `forest`, `mono`, `breeze`, `candy`, `crimson`, `falcon`, `meadow`, `midnight`, `raindrop`, `sunset`, `rabbit`, `firecrawl`, `aws`, `auth0` |28| `darkMode` | `true` | `true`, `false` |29| `background` | `true` | `true`, `false` |30| `padding` | `64` | `16`, `32`, `64`, `128` |31| `language` | auto-detect | `javascript`, `typescript`, `tsx`, `jsx`, `python`, `go`, `rust`, `swift`, `shell`, `css`, `html`, `json`, `sql`, `ruby`, `java`, `kotlin`, `cpp`, `csharp`, `dart`, `elixir`, `graphql`, `haskell`, `lua`, `markdown`, `php`, `scala`, `toml`, `yaml`, `zig`, `diff`, `dockerfile`, `plaintext`, and more |32| `title` | `""` | Filename shown in the title bar |33| `subtitle` | `""` | Subtitle text |34| `width` | auto | Pixel width (number) |35| `lineNumbers` | `undefined` | `true`, `false` |36| `highlightedLines` | `""` | Comma-separated line numbers |3738## Workflow: generate and save a code image3940Single Playwriter call. Use the script below **exactly as-is**, only changing the41`code`, `language`, `title`, `theme`, and output path variables. Do not restructure,42reorder, or simplify the script; the clipboard interceptor must be injected before43`Cmd+C` is pressed, and the poll loop must run after.4445```bash46playwriter -s <sessionId> --timeout 30000 -e "$(cat <<'PYEOF'47const code = `export function hello() {48 return "world"49}`50const encoded = Buffer.from(code).toString('base64url')51const params = new URLSearchParams()52params.set('code', encoded)53params.set('theme', 'vercel')54params.set('darkMode', 'true')55params.set('background', 'true')56params.set('padding', '64')57params.set('language', 'typescript')58params.set('title', 'hello.ts')5960// Navigate (always fresh page)61state.page = await context.newPage()62await state.page.goto('https://ray.so/#' + params.toString(), { waitUntil: 'domcontentloaded' })63await state.page.waitForSelector('#frame', { timeout: 10000 })6465// Intercept clipboard write to capture the PNG blob66await state.page.evaluate(() => {67 window.__clipboardBlob = null68 const origWrite = navigator.clipboard.write.bind(navigator.clipboard)69 navigator.clipboard.write = async (items) => {70 for (const item of items) {71 if (item.types.includes('image/png')) {72 window.__clipboardBlob = await item.getType('image/png')73 }74 }75 return origWrite(items)76 }77})7879// Press Cmd+C and poll until the blob is captured (no fixed sleep)80await state.page.keyboard.press('Meta+c')81let attempts = 082while (attempts < 50) {83 const hasBlob = await state.page.evaluate(() => !!window.__clipboardBlob)84 if (hasBlob) break85 await state.page.waitForTimeout(100)86 attempts++87}88if (attempts >= 50) throw new Error('Clipboard blob not captured after 5s')8990// Read blob as base64 and save via Node.js fs91const base64 = await state.page.evaluate(async () => {92 const blob = window.__clipboardBlob93 const buf = await blob.arrayBuffer()94 const bytes = new Uint8Array(buf)95 let binary = ''96 for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i])97 return btoa(binary)98})99100const fs = require('node:fs')101const buffer = Buffer.from(base64, 'base64')102fs.writeFileSync('/absolute/path/to/output.png', buffer)103console.log('Saved', buffer.length, 'bytes')104105// Close the page106await state.page.close()107PYEOF108)"109```110111The clipboard poll checks every 100ms until the PNG blob is captured, with a 5s timeout.112After saving, verify the output image with the `read-media` tool.113114## Agent rules115116- Always use `vercel` theme unless the user asks for something else117- Always use `darkMode=true`, `background=true`, `padding=64` by default118- Set `language` explicitly when known; do not rely on auto-detect119- Set `title` to the filename when available (e.g. `greet.ts`)120- Use absolute paths for the output PNG121- Do **not** use `js-base64` npm package in Playwriter; use `Buffer.from(code).toString('base64url')` instead (Playwriter sandbox only allows Node.js built-ins)122- Do **not** inspect the page between navigate and `Cmd+C`; the URL params are deterministic123- After saving, verify the image with `read-media` tool to confirm it rendered correctly