From eec65b95e9517e39e28bc2fe171b0d8ce73ec7ee Mon Sep 17 00:00:00 2001 From: jktrn-osec Date: Thu, 11 Jun 2026 02:45:49 -0700 Subject: [PATCH] fix: site href, banners in og generator --- docs-v2/api/mcp.ts | 37 ++++-- docs-v2/astro.config.ts | 7 +- docs-v2/src/consts.ts | 2 +- .../spl-token-basics/create-token-account.mdx | 4 +- .../docs/v2/reference/feature-flags.mdx | 10 +- .../docs/v2/security/raw-account-access.mdx | 18 +-- docs-v2/src/pages/og/[...slug].png.ts | 117 ++++++++++++++++-- 7 files changed, 159 insertions(+), 36 deletions(-) diff --git a/docs-v2/api/mcp.ts b/docs-v2/api/mcp.ts index e955376ce6..e779eae288 100644 --- a/docs-v2/api/mcp.ts +++ b/docs-v2/api/mcp.ts @@ -17,19 +17,25 @@ async function getDocs(origin: string): Promise { const TOOLS = [ { name: 'search_anchor_docs', - description: 'Search the Anchor (anchor-lang) docs. Returns matching pages with title, path, url, snippet.', + description: + 'Search the Anchor (anchor-lang) docs. Returns matching pages with title, path, url, snippet.', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] }, }, { name: 'read_anchor_doc', - description: 'Read the full Markdown of an Anchor docs page by its path (from search results), e.g. "v2/fundamentals/pda".', + description: + 'Read the full Markdown of an Anchor docs page by its path (from search results), e.g. "v2/fundamentals/pda".', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, }, { name: 'query_docs_filesystem_anchor', description: 'Run a read-only shell command (rg, grep, find, tree, ls, cat, head, sed, awk, jq, pipes, &&) against an in-memory filesystem of the Anchor docs rooted at /. Pages are at /.mdx. No network, no persisted writes.', - inputSchema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] }, + inputSchema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, }, ] @@ -53,14 +59,20 @@ function search(docs: Doc[], query: string, limit = 8) { } async function shell(docs: Doc[], command: string): Promise { - const files = Object.fromEntries(docs.map((d) => [`/${d.id}.mdx`, `Title: ${d.title}\n${d.description}\n\n${d.body}`])) + const files = Object.fromEntries( + docs.map((d) => [`/${d.id}.mdx`, `Title: ${d.title}\n${d.description}\n\n${d.body}`]), + ) //this is a fake shell to make it easier for the agent to interact, nothing too fancy const { stdout, stderr, exitCode } = await new Bash({ files, cwd: '/', executionLimits: { maxCommandCount: 200, maxLoopIterations: 10_000 }, }).exec(command) - return `exit: ${exitCode}` + (stdout ? `\n--- stdout ---\n${stdout}` : '') + (stderr ? `\n--- stderr ---\n${stderr}` : '') + return ( + `exit: ${exitCode}` + + (stdout ? `\n--- stdout ---\n${stdout}` : '') + + (stderr ? `\n--- stderr ---\n${stderr}` : '') + ) } const ok = (id: unknown, result: unknown) => @@ -93,7 +105,12 @@ export async function POST(request: Request) { const q = String(args.query ?? '') const hits = search(docs, q) const text = hits.length - ? hits.map((d) => `## ${d.title}\npath: ${d.id}\nurl: ${d.url}\n${d.description || d.body.slice(0, 200)}`).join('\n\n') + ? hits + .map( + (d) => + `## ${d.title}\npath: ${d.id}\nurl: ${d.url}\n${d.description || d.body.slice(0, 200)}`, + ) + .join('\n\n') : `No results for "${q}".` return ok(m.id, { content: [{ type: 'text', text }] }) } @@ -101,12 +118,16 @@ export async function POST(request: Request) { const path = String(args.path ?? '').replace(/^\/+|\/+$|\.mdx?$/g, '') const d = docs.find((x) => x.id === path || x.id === `${path}/index`) return ok(m.id, { - content: [{ type: 'text', text: d ? `# ${d.title}\n${d.url}\n\n${d.body}` : `Not found: ${path}` }], + content: [ + { type: 'text', text: d ? `# ${d.title}\n${d.url}\n\n${d.body}` : `Not found: ${path}` }, + ], isError: !d, }) } if (name === 'query_docs_filesystem_anchor') - return ok(m.id, { content: [{ type: 'text', text: await shell(docs, String(args.command ?? '')) }] }) + return ok(m.id, { + content: [{ type: 'text', text: await shell(docs, String(args.command ?? '')) }], + }) return fail(m.id, -32602, `Unknown tool: ${name}`) } diff --git a/docs-v2/astro.config.ts b/docs-v2/astro.config.ts index 9e90ed0e8f..e6cc600250 100644 --- a/docs-v2/astro.config.ts +++ b/docs-v2/astro.config.ts @@ -24,7 +24,10 @@ import { darkTheme, lightTheme } from './src/lib/shiki-themes' type DevMiddleware = ( req: { url?: string }, - res: { setHeader(name: string, value: string): void; end(data: Uint8Array): void }, + res: { + setHeader(name: string, value: string): void + end(data: Uint8Array): void + }, next: () => void, ) => void | Promise @@ -87,7 +90,7 @@ function resolvePagefindAsset(url: string): string | null { } export default defineConfig({ - site: 'https://www.anchor-lang.com', + site: 'https://v2.anchor-lang.com', base: DOCS_BASE, trailingSlash: 'always', outDir: './dist/docs', diff --git a/docs-v2/src/consts.ts b/docs-v2/src/consts.ts index 0525d05543..ee1677dceb 100644 --- a/docs-v2/src/consts.ts +++ b/docs-v2/src/consts.ts @@ -3,7 +3,7 @@ import type { DocsConfig, IconMap, Site, SocialLink } from '@/types' export const SITE: Site = { title: 'Anchor Docs', description: 'Anchor is the leading development framework for building Solana programs.', - href: 'https://www.anchor-lang.com', + href: 'https://v2.anchor-lang.com', author: 'solana-foundation', locale: 'en-US', } diff --git a/docs-v2/src/content/docs/v1/tokens/spl-token-basics/create-token-account.mdx b/docs-v2/src/content/docs/v1/tokens/spl-token-basics/create-token-account.mdx index 22d358d925..73c55ad5e2 100644 --- a/docs-v2/src/content/docs/v1/tokens/spl-token-basics/create-token-account.mdx +++ b/docs-v2/src/content/docs/v1/tokens/spl-token-basics/create-token-account.mdx @@ -58,8 +58,8 @@ in the `owner` field of the `Account{:rs}` type defined by the Token Program. Th Program or Token Extension Program, as specified in the `owner` field of the base Solana [`Account{:rs}`](https://github.com/anza-xyz/agave/blob/master/sdk/account/src/lib.rs#L44-L56) type. - When working with token accounts, "owner" typically refers to the authority that can spend the - tokens, not the program that owns the account. +When working with token accounts, "owner" typically refers to the authority that can spend the tokens, not the program that owns the account. + ## What is an associated token account? diff --git a/docs-v2/src/content/docs/v2/reference/feature-flags.mdx b/docs-v2/src/content/docs/v2/reference/feature-flags.mdx index a4a5a851f7..fdcb04e68d 100644 --- a/docs-v2/src/content/docs/v2/reference/feature-flags.mdx +++ b/docs-v2/src/content/docs/v2/reference/feature-flags.mdx @@ -70,11 +70,11 @@ The `compat{:toml}` feature exposes a small set of v1-shaped helpers for program The remaining helpers re-expose v1 names so existing call sites compile without rewrites: -| Helper | Form | -| --- | --- | -| `error!{:rs}`, `err!{:rs}`, `pubkey!{:rs}` | Macros | -| `Pubkey{:rs}` | Type alias for `Address{:rs}` | -| `AccountViewCompat{:rs}` | Extension trait on pinocchio's `AccountView{:rs}` | +| Helper | Form | +| ------------------------------------------ | ------------------------------------------------- | +| `error!{:rs}`, `err!{:rs}`, `pubkey!{:rs}` | Macros | +| `Pubkey{:rs}` | Type alias for `Address{:rs}` | +| `AccountViewCompat{:rs}` | Extension trait on pinocchio's `AccountView{:rs}` | `debug!{:rs}` is meaningfully more expensive than the native `msg!{:rs}` because of the heap allocation and fmt-trait dispatch. The feature is off by default to keep production binaries from accidentally shipping the heavier path. diff --git a/docs-v2/src/content/docs/v2/security/raw-account-access.mdx b/docs-v2/src/content/docs/v2/security/raw-account-access.mdx index e8311563a2..103e41a833 100644 --- a/docs-v2/src/content/docs/v2/security/raw-account-access.mdx +++ b/docs-v2/src/content/docs/v2/security/raw-account-access.mdx @@ -42,15 +42,15 @@ A program that drops `#[derive(Accounts)]{:rs}` owns every check itself. The pin When you do go raw, put back each check Anchor would have run: -| Check Anchor runs | Put it back with | -| --- | --- | -| Account count | An `accounts.len() < N{:rs}` guard before any unchecked indexing | -| Discriminator | Compare the first byte before trusting the rest | -| Owner | Compare `view.owner(){:rs}` against the program id | -| Writable | Check `view.is_writable(){:rs}` before a mutable borrow | -| Signer | Check `signer.is_signer(){:rs}` | -| Borrow tracking | Ensure no other `&mut{:rs}` to the account is live | -| PDA derivation | `find_program_address{:rs}`, or a `const{:rs}` bump for literal seeds | +| Check Anchor runs | Put it back with | +| ----------------- | --------------------------------------------------------------------- | +| Account count | An `accounts.len() < N{:rs}` guard before any unchecked indexing | +| Discriminator | Compare the first byte before trusting the rest | +| Owner | Compare `view.owner(){:rs}` against the program id | +| Writable | Check `view.is_writable(){:rs}` before a mutable borrow | +| Signer | Check `signer.is_signer(){:rs}` | +| Borrow tracking | Ensure no other `&mut{:rs}` to the account is live | +| PDA derivation | `find_program_address{:rs}`, or a `const{:rs}` bump for literal seeds | An init path can lean on `create_account_signed{:rs}`, which enforces the PDA match and the program owner as it creates the account. A read-after-init path has no such helper, so re-check owner and discriminator by hand. diff --git a/docs-v2/src/pages/og/[...slug].png.ts b/docs-v2/src/pages/og/[...slug].png.ts index bcb98fa1ea..c21a3d8475 100644 --- a/docs-v2/src/pages/og/[...slug].png.ts +++ b/docs-v2/src/pages/og/[...slug].png.ts @@ -1,10 +1,12 @@ import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' +import { BANNER_GRAPHICS, getBannerGraphic, type BannerGraphic } from '@/lib/banner-graphics' import { docSlugFromId, getAllDocs, type Doc } from '@/lib/docs' import { darkTheme, lightTheme } from '@/lib/shiki-themes' import type { MetaFile } from '@/types' import { ImageResponse } from '@vercel/og' import React from 'react' +import sharp from 'sharp' import { codeToTokens } from 'shiki' import type { BundledLanguage } from 'shiki' @@ -13,6 +15,11 @@ const size = { height: 630, } +const bannerPanel = { + width: 1200, + height: 210, +} + const dmSansRegular = readFile(resolve(process.cwd(), 'src/assets/fonts/DMSans-Regular.ttf')) const dmSansMedium = readFile(resolve(process.cwd(), 'src/assets/fonts/DMSans-Medium.ttf')) const cascadiaCodeRegular = readFile( @@ -47,16 +54,25 @@ interface Props { export async function GET({ props }: { props: Props }) { const { doc } = props + const banner = resolveBannerGraphic(doc) const theme = resolveTheme(doc.id) - const [title, description, wordmark, dmSansRegularData, dmSansMediumData, cascadiaCodeData] = - await Promise.all([ - renderInlineText(doc.data.title, theme), - doc.data.description ? renderInlineText(doc.data.description, theme) : null, - svgDataUrl(theme.dark ? wordmarkDark : wordmarkLight), - dmSansRegular, - dmSansMedium, - cascadiaCodeRegular, - ]) + const [ + title, + description, + bannerImage, + wordmark, + dmSansRegularData, + dmSansMediumData, + cascadiaCodeData, + ] = await Promise.all([ + renderInlineText(doc.data.title, theme), + doc.data.description ? renderInlineText(doc.data.description, theme) : null, + bannerDataUrl(resolve(process.cwd(), 'public', banner.src.replace(/^\//, '')), banner), + svgDataUrl(theme.dark ? wordmarkDark : wordmarkLight), + dmSansRegular, + dmSansMedium, + cascadiaCodeRegular, + ]) const breadcrumb = breadcrumbParts(doc.id) return new ImageResponse( @@ -78,9 +94,29 @@ export async function GET({ props }: { props: Props }) { React.createElement( 'div', { + style: { + width: '100%', + height: bannerPanel.height, + display: 'flex', + overflow: 'hidden', + borderBottom: `1px solid ${theme.border}`, + }, + }, + React.createElement('img', { + src: bannerImage, + alt: banner.description, style: { width: '100%', height: '100%', + }, + }), + ), + React.createElement( + 'div', + { + style: { + width: '100%', + height: size.height - bannerPanel.height, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', @@ -188,6 +224,29 @@ export async function GET({ props }: { props: Props }) { ) } +async function bannerDataUrl(path: string, banner: BannerGraphic): Promise { + const metadata = await sharp(path).metadata() + + if (!metadata.width || !metadata.height) { + throw new Error(`Unable to read banner dimensions for ${banner.src}`) + } + + const crop = coverCrop( + metadata.width, + metadata.height, + bannerPanel.width, + bannerPanel.height, + banner, + ) + const data = await sharp(path) + .extract(crop) + .resize(bannerPanel.width, bannerPanel.height) + .jpeg({ quality: 86 }) + .toBuffer() + + return `data:image/jpeg;base64,${data.toString('base64')}` +} + async function svgDataUrl(svg: Promise): Promise { const data = await svg return `data:image/svg+xml;base64,${Buffer.from(data).toString('base64')}` @@ -305,6 +364,38 @@ async function renderInlineCode( ) } +function resolveBannerGraphic(doc: Doc): BannerGraphic { + if (typeof doc.data.banner === 'string' && doc.data.banner !== 'random') { + return getBannerGraphic(doc.data.banner) + } + + return BANNER_GRAPHICS[hashString(doc.id) % BANNER_GRAPHICS.length] +} + +function coverCrop( + sourceWidth: number, + sourceHeight: number, + targetWidth: number, + targetHeight: number, + banner: BannerGraphic, +) { + const [xPercent, yPercent] = banner.objectPosition + .split(' ') + .map((value) => Number.parseFloat(value) / 100) + + const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight) + const width = Math.min(sourceWidth, Math.round(targetWidth / scale)) + const height = Math.min(sourceHeight, Math.round(targetHeight / scale)) + const left = Math.round(clamp((sourceWidth - width) * xPercent, 0, sourceWidth - width)) + const top = Math.round(clamp((sourceHeight - height) * yPercent, 0, sourceHeight - height)) + + return { left, top, width, height } +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max) +} + function resolveTheme(id: string) { const dark = id === 'v1' || id.startsWith('v1/') @@ -333,6 +424,14 @@ function resolveTheme(id: string) { } } +function hashString(input: string): number { + let hash = 0 + for (const char of input) { + hash = (hash * 31 + char.charCodeAt(0)) >>> 0 + } + return hash +} + function titleSize(title: string): number { if (title.length > 70) return 50 if (title.length > 52) return 56