尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证dynamic image serving for gh readmes
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Live, theme-aware, animated GitHub stat cards for README embeds.
One URL. Adapts to the viewer's reader. Animates inside camo. Works everywhere <img> works.
Built on Next.js 16 · Edge SVG + Satori · Skia + Sharp on Node · Zero client JS
The card system was rebuilt from the data layer up for long-term extensibility — typed atoms, Zod-schema-driven cards, a /api/meta introspection endpoint, and a new fetch-less text card primitive proving the architecture handles non-GitHub data sources cleanly. Existing cards (commits, streak, portrait) below render through the new pipeline at unchanged URLs.
text card↑ /api/_/text.svg?text=composable%20by%20design&size=44&theme=ocean&w=520&h=100 · format=svg · runtime=edge · renderer=browser · 🎬 none · zero upstream fetch — the input IS the data
↑ /api/_/text.png?text=hello%20from%20Phase%201&size=32&theme=ember&w=400&h=80 · format=png · runtime=edge · renderer=Satori · 🖼️ static
↑ text=%E2%9C%A6%20left-aligned%20in%20rose&align=left · format=svg · runtime=edge · the placeholder _ in the path is ignored — text cards have no user dimension
The future editor reads this; you can too:
curl https://dynimage.vercel.app/api/meta | jq '.cards[] | {name, runtime, formats, dimensions: .meta.dimensions}'
Returns every card type, its declared runtime, supported formats, and a Zod-derived JSON Schema for its input — the editor will use this to render per-card forms without hardcoding any card name.
/api/renderMultiple cards composed into one image via a single base64-encoded layout spec. The URL is fully cacheable by camo — same as a regular card embed, just bigger.
↑ /api/render?format=png&spec=<base64-of-LayoutSpec> · 5 sub-cards (1 text + 2 metric + 2 bar) composed by sharp · 800×320 · ocean theme · URL is 797 chars total
{
"v": 1,
"w": 800,
"h": 320,
"theme": "ocean",
"cards": [
{
"type": "text",
"input": { "text": "moefingers", "size": 36, "align": "left" },
"x": 16,
"y": 16,
"w": 280,
"h": 60
},
{
"type": "metric",
"input": { "label": "Commits last year", "value": "1247" },
"x": 320,
"y": 16,
"w": 220,
"h": 140
},
{
"type": "metric",
"input": { "label": "Active streak", "value": "42", "unit": "days" },
"x": 560,
"y": 16,
"w": 220,
"h": 140
},
{
"type": "bar",
"input": { "label": "Authored", "value": 47, "max": 60 },
"x": 16,
"y": 180,
"w": 380,
"h": 80
},
{
"type": "bar",
"input": { "label": "Test coverage", "value": 78, "max": 100 },
"x": 410,
"y": 180,
"w": 380,
"h": 80
}
]
}
GET — for <img src="…"> embeds. URL contains the entire spec; camo caches like any image:
GET /api/render?format=png&spec=<base64-of-LayoutSpec>
GET /api/render?format=svg&spec=<base64> # vector + animation preserved
GET /api/render?format=avif&spec=<base64> # smallest payload
GET /api/render?format=png&spec=<base64>&z=1 # set z=1 if spec is gzipped before base64
GET /api/render?format=svg&scene=<base64url-of-Scene> # scene/element model
GET /api/render?format=svg&c=<base64url(gzip(json))>&z=1 # Tier-2 compressed blob (editor output)
?c= is the compressed sibling of ?scene=: the codec unwraps its versioned
envelope to a Scene and renders the identical output (z=1 when gzipped;
omit z for small payloads the encoder ships raw).
POST — for server-to-server callers (sync scripts, build tools). JSON body, image bytes back:
curl -X POST 'https://dynimage.vercel.app/api/render?format=png' \
-H 'content-type: application/json' \
-d @spec.json \
--output header.png
[!NOTE] These cards hit the live API at
dynimage.vercel.app. The SVG variants animate inside GitHub'scamoimage proxy via SMIL + CSS keyframes — only<script>is stripped, animation is not.
Each card is labeled below with its format, the runtime that serves it, and the renderer that produces the pixels. When debugging a broken card, the label tells you exactly which path you're looking at.
[!NOTE] SVG outputs ship raw markup that your browser renders — fonts, emoji, SMIL animation, and
prefers-color-schememedia queries all use the viewer's OS capabilities. Raster outputs (PNG/WebP/AVIF) are pre-rendered on the server by Satori, sharp, or Skia, and ship as flat pixels — server-side font availability matters.
commits — Edge runtime↑ commits.svg · format=svg · runtime=edge · renderer=browser (raw SVG passthrough) · 🎬 animated (SMIL count-up + CSS keyframes + sheen sweep) · <picture> swaps by prefers-color-scheme
↑ commits.png?theme=ocean · format=png · runtime=edge · renderer=Satori (next/og, JSX → SVG → PNG via WASM) · 🖼️ static (raster — no animation possible)
↑ commits.png?theme=ember · format=png · runtime=edge · renderer=Satori · 🖼️ static
streak — Node runtime↑ streak.svg · format=svg · runtime=node · renderer=browser (raw SVG passthrough) · 🎬 animated (SMIL pulse rings, real Gaussian blur via SVG <filter>, CSS keyframe entrance)
↑ streak.png?theme=forest · format=png · runtime=node · renderer=sharp (rasterizes the same SVG via librsvg) · 🖼️ static · text rendered via Noto Sans inlined as @font-face data URI; flame is an inline SVG <path> (Heroicons)
↑ streak.png?theme=rose · format=png · runtime=node · renderer=sharp · 🖼️ static
portrait — Node runtime↑ portrait.png · format=png · runtime=node · renderer=Skia (@napi-rs/canvas, imperative Canvas API with avatar compositing) · 🖼️ static
↑ portrait.avif?theme=ember&w=900&h=400 · format=avif · runtime=node · renderer=Skia → sharp (Skia renders, sharp transcodes to AVIF) · 🖼️ static
commits.png (Satori) — bundles a default font internally, no project-side setup neededstreak.png (sharp via librsvg) — bundles NotoSans-Regular.ttf under src/lib/cards/fonts/ and inlines it as @font-face data URI in the SVG at render time; the 🔥 emoji was replaced with an inline SVG flame <path> so no emoji font is needed server-sideportrait.png/.webp/.avif (Skia via @napi-rs/canvas) — uses Skia's internal font registry; falls back gracefully to system fontsThe same card, six themes. Override any color via query string with raw hex (no leading #).

flowchart LR
GH["GitHub README<br/><img src=...>"] --> CAMO["camo<br/>image proxy"]
CAMO --> RW{"next.config<br/>rewrites"}
RW -->|"commits.*"| EDGE["api/<user>/<stat><br/>Edge runtime"]
RW -->|"streak.* + portrait.*"| NODE["api/n/<user>/<stat><br/>Node runtime"]
EDGE --> DISP["dispatchCard"]
NODE --> DISP
DISP --> REG{"card registry"}
REG -->|"commits.svg + streak.svg"| TPL["template SVG<br/>SMIL + CSS"]
REG -->|"commits.png"| SATORI["next/og<br/>Satori → PNG"]
REG -->|"streak.png"| RSHARP["sharp<br/>rasterize SVG + blur filter"]
REG -->|"portrait.png"| SKIA["@napi-rs/canvas<br/>(Skia)"]
REG -->|"portrait.webp + .avif"| SKIASHARP["Skia → sharp<br/>transcode"]
EDGE -.->|"GraphQL"| GQL["@octokit/graphql<br/>+ Next fetch cache"]
NODE -.-> GQL
GQL -.-> GHAPI["GitHub API"]
/api/<user>/<card>.<format>?<query>
| segment | values |
|---|---|
card | commits · streak · portrait |
format | svg · png · webp · avif (per card; not all support all) |
theme | dark · light · ocean · ember · forest · rose |
bg, accent, text, text-muted, gradient, stroke | hex without #, overrides theme |
w, h | 1..4096 |
Press R in your browser to bust the cache and see fresh data.
[!TIP] GitHub's
camoproxy serves SVG faithfully —<script>is stripped, but SMIL animation,<style>blocks, CSS@keyframes,@media (prefers-color-scheme), and<foreignObject>all work. dynimage uses all of them.
dynimage's SVG output ships with:
<animateTransform> sheen sweeps, <animate> count-up, pulse rings<style> — entrance fades, transitionsprefers-color-scheme media queries when no theme param is given — one URL, both modes<filter> with feGaussianBlur — actual Gaussian blur, not fakedgit clone https://github.com/moefingers/dynimage
cd dynimage
pnpm install
cp .env.example .env.local # add your GITHUB_TOKEN
pnpm dev # http://localhost:3000
# Production
vercel link
vercel env add GITHUB_TOKEN production preview development
vercel --prod
The GITHUB_TOKEN must be a fine-grained PAT with read-only public repo metadata access. Create one at github.com/settings/tokens?type=beta.
[!IMPORTANT] The cards above will return
502untilGITHUB_TOKENis set in the Vercel project envs and the project is redeployed.
src/lib/cards/<name>.ts exporting a Card<TData> — data fetcher, renderer per format, declared runtimesrc/lib/cards/registry-edge.ts or registry-node.ts (pick the one matching your runtime) and to registry-meta.ts for the homepage demo gridruntime: 'nodejs', add the new card name to the regex group in next.config.ts rewrites| layer | choice | why |
|---|---|---|
| Framework | Next.js 16 App Router | dual runtime per route, file-based catch-all, next/og built in |
| Type system | TypeScript strict + noUncheckedIndexedAccess | per zcanon canon |
| Edge PNG | next/og (Satori + Resvg) | Vercel-native, JSX-driven, no native deps |
| Edge real-blur PNG | @resvg/resvg-wasm | hand-authored SVG with <filter feGaussianBlur> rasterized on Edge |
| Node PNG | @napi-rs/canvas (Skia) | full Canvas API, same engine as Chrome |
| Node transcode | sharp | AVIF/WebP, best compression |
| GitHub data | @octokit/graphql + next: { revalidate: 300 } | one round-trip per card, shared cache |
| Package manager | pnpm | per zcanon canon |
Why does the streak's blur look right at any DPR? Because SVG <filter> operates in user-space units, which the rasterizer scales:
$$ \sigma_{\text{px}} = \sigma_{\text{user}} \cdot \frac{w_{\text{px}}}{w_{\text{user}}} $$
The blur kernel grows with the output resolution, so the visual blur radius stays constant. No "looks fuzzy at 4x" artifacts.
| Technique | Where |
|---|---|
<picture> with <source media> | commits theme-responsive embed at top |
| Animated SVG embed | streak, commits |
prefers-color-scheme inside the SVG | when no theme= param |
> [!NOTE] / [!TIP] / [!IMPORTANT] callouts | throughout |
<details> / <summary> collapsibles | setup, adding cards, this section |
<kbd> keyboard chips | URL shape section |
<div align="center"> heading | top of README |
<sub> for sub-text | top of README |
| Mermaid diagram | architecture section |
LaTeX math ($$...$$) | the "why blur scales correctly" section above |
Repo: github.com/moefingers/dynimage · API: dynimage.vercel.app