HivePaaS
Docker Swarm 기반 오픈소스 자체 호스팅 PaaS, SSL·백업·자동 확장 지원
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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