> ## Content Index
> Fetch the complete content index at: https://developer-service.blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# I Vibe-Coded a Shields.io Clone - Then I Wouldn't Ship It
- URL: https://developer-service.blog/i-vibe-coded-a-shields-io-clone-then-i-wouldnt-ship-it/
- Published: 2026-10-26T09:23:52.000Z
- Updated: 2026-10-26T09:23:52.000Z
- Description: Claude Code, Cursor or Codex can, at this point in time build a very accurate clone for most applications, but that doesn't mean that it is ready for you to put that URL on the public internet, and that gap is why I wrote this down.
- Author: Nuno Bispo
- Tags: AI

I asked the AI for a simple Shields.io clone in FastAPI. Twenty minutes later I had a green badge in the browser.

Left side grey: build. Right side green: passing.

Same silhouette you paste into a README without thinking. For a second, it felt almost finished, the kind of finished that makes you open a repo and type `README.md`.

![](https://developer-service.blog/content/images/2026/10/build-passinsg-screenshot.png)

The example in this article is simplistic by nature, but the principles are valid for a more complex AI build.

Claude Code, Cursor or Codex can, at this point in time build a very accurate clone for most applications, but that doesn't mean that it is ready for you to put that URL on the public internet, and that gap is why I wrote this down.

---

## The bet

I kept the surface small on purpose. Flat badges only: a URL like `/badge/{label}-{message}-{color}`, one FastAPI route, SVG out. No style variants, no logo endpoints, no shields.io feature zoo. If the path looked like `build-passing-green`, the response should look like the badge everyone already recognizes.

I also told the model to vibe-code the first version: prompt hard, accept the first pass that renders, harden after. Not “design a careful architecture”. Roughly what I pasted:

```text
FastAPI app: GET /badge/{label}-{message}-{color} returns a flat
Shields-style SVG. Keep it tiny. Fake the widths if you have to.

```

I wanted the thing that shows up in twenty minutes when you describe the screenshot and hit enter, because that’s what most of these clones actually are, even when the write-up pretends otherwise.

What I was *not* testing: deploy, auth, multi-region anything. Narrower question: can an AI give you a demo that looks done, and can you still name what you’d refuse to expose before anyone else’s README depends on it?

---

## What “done” looked like

The happy path was almost rude in how little it needed. One route. Split the trailing path on `-`. Map a handful of color names. Stuff label and message into an SVG string. Return it with the right content type so the browser renders a badge instead of XML soup.

```python
from fastapi import FastAPI, Response

app = FastAPI()

COLORS = {
    "green": "#4c1",
    "brightgreen": "#4c1",
    "red": "#e05d44",
    "blue": "#007ec6",
    "grey": "#555",
    "gray": "#555",
}

@app.get("/badge/{badge_path}")
def badge(badge_path: str) -> Response:
    label, message, color = badge_path.rsplit("-", 2)
    fill = COLORS.get(color, "#555")
    # Fake widths — good enough for build-passing-green, wrong for longer text
    left_w, right_w = 50, 60
    width = left_w + right_w
    svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="20">
  <rect width="{left_w}" height="20" fill="#555"/>
  <rect x="{left_w}" width="{right_w}" height="20" fill="{fill}"/>
  <text x="{left_w // 2}" y="14" fill="#fff" font-size="11"
        text-anchor="middle" font-family="DejaVu Sans,Verdana,sans-serif">{label}</text>
  <text x="{left_w + right_w // 2}" y="14" fill="#fff" font-size="11"
        text-anchor="middle" font-family="DejaVu Sans,Verdana,sans-serif">{message}</text>
</svg>'''
    return Response(content=svg, media_type="image/svg+xml")

```

Hit `/badge/build-passing-green`. Green and satisfying, the kind of result that makes vibe-coding feel like cheating in a good way.

That `rsplit("-", 2)` and those hardcoded widths are doing a lot of silent lying. They work until they don’t.

---

## Where the demo lies

The badge looked right because I tested the one URL everyone tests. Production is the other URLs.

### 1\. The path is not a triple

`rsplit("-", 2)` feels clever until a field itself contains a hyphen. You meant label `build`, message `pre-release`, color `orange`. The path is `/badge/build-pre-release-orange`. The stub returns:

```text
label="build-pre"  message="release"  color="orange"

```

Wrong badge, still a 200\. Same class of pain: `coverage-95%` (that `%` wants encoding) and anything else a README will mangle. In the browser you typed a clean string. In a README, someone will paste chaos.

Shields.io escapes inside the path so hyphens stay data, not delimiters: `_` for a space, `--` for a literal `-`. So `pre-release` becomes `pre--release`, and `/badge/build-pre--release-orange` still splits into three fields. My stub has none of that.

Before I’d expose it: define the split rules (and the failures) in tests. A demo that only accepts `build-passing-green` is a screenshot generator.

### 2\. Colors are a tiny language

`green` and `brightgreen` both mapped to `#4c1` in my stub. Fine for a demo. Real traffic sends `brightgreen`, `#4c1`, `4c1`, and typos. Unknown names silently becoming grey is a nice default until a CI badge says “passing” in the color of “who knows”.

Before I’d expose it: one explicit color table, hex parsing with a hard reject, and no “close enough” fallbacks that lie about status.

### 3\. Width is the tell

Hardcoded `50` and `60` make short English labels look professional. Hit `/badge/continuous-integration-passing-green` and the stub still draws a 50px grey rect. At 11px DejaVu, `continuous-integration` wants roughly 140px. The letters run out of the pill; the green side stays pinned at 60\. Looks broken in the browser, still a 200.

Real badge services measure text. My stub skipped that.

Minimum I’d demand: measure (or approximate) label/message width and size the rects from that. If you’re still faking widths, say so in the README. Don’t imply parity with shields.io.

### 4\. Caching is part of the contract

A raw FastAPI `Response` with an SVG body will get hit on every README render unless you say otherwise. No `Cache-Control`, no `ETag`, and you’ve built a chatty origin for other people’s traffic patterns.

Ship gate: decide what “the badge changed” means, then set headers on purpose. For a static flat badge I’d start here:

```python
return Response(
    content=svg,
    media_type="image/svg+xml",
    headers={"Cache-Control": "public, max-age=60"},
)

```

Sixty seconds is arbitrary. Leaving the header off is a decision too, usually a bad one.

### 5\. Abuse shows up before features do

Hotlinking is the product. That’s the point of a badge URL. Without rate limits, size caps, or a boring allowlist of what you’ll render, you’re volunteering as free image hosting for the internet’s worst README experiments. Add remote logos later and you’ve bought an SSRF-shaped problem (your server fetching a URL an attacker chose) while chasing a shiny `/logo/` route.

Before I’d expose it: cap path length, reject junk early, rate-limit by IP at minimum, and don’t fetch anything remote until you have a reason stronger than “shields.io has logos”.

---

## Would I ship it?

I would not ship this version as a public URL that other people’s READMEs depend on.

I would show it in a terminal, keep it on localhost, and use it to argue with AI about path parsing and text width. I would not paste `https://my-box/badge/build-passing-green` into a repo that strangers clone.

Vibe-coding still did its job. The model got me a convincing SVG faster than I would have typed it. I started asking harder questions when the badge looked done: what did I still not trust?

If you are evaluating whether AI ships useful things or only demos, steal the harden list into your next review. Treat the green rectangle as a prototype receipt.

Twenty minutes for the SVG. The rest is the list above.

---

## When I’d do this again

I’d do it again when the browser answers in seconds and the whole thing fits in one route. Badges. Small SVG toys. A JSON endpoint that either returns or doesn’t.

If “it rendered” doesn’t teach you something immediately, pick a different toy. What I’d repeat is writing the harden list while the demo still feels finished, the list you’d actually defend in review.

*Full source code:* [*shields-vibe-clone*](https://github.com/nunombispo/shields-vibe-clone?ref=developer-service.blog)*.*

---

Follow me on Twitter: [https://twitter.com/DevAsService](https://twitter.com/DevAsService?ref=developer-service.blog)

Follow me on Instagram: [https://www.instagram.com/devasservice/](https://www.instagram.com/devasservice/?ref=developer-service.blog)

Follow me on TikTok: [https://www.tiktok.com/@devasservice/](https://www.tiktok.com/@devasservice/?ref=developer-service.blog)

Follow me on YouTube: [https://www.youtube.com/@DevAsService](https://www.youtube.com/@DevAsService?ref=developer-service.blog)