A Claude Code /logo skill built from five wrong turns — SVG paths, regex bounds, a wrong kanji
Redesigning this blog's logo and code font with an AI agent: a broken SVG letterform, a regex bounding box, a wrong kanji, and the /logo skill it produced.

On this page
Introduction
I sat down to fix a logo I had not been happy with for a long time: a plain text mark this project generated for itself on day one, and that nobody had made a decision about since.
Designing a logo with an AI agent takes rounds: the agent generates candidates, I look, I cut and say why, and we go again. What I wanted to know was whether that loop was worth writing down as a Claude Code skill.
The agent was
confidently wrong five separate times across those rounds, and I count none of
it as wasted. Four of the five wrong turns named a rule, and the rules are now
a /logo skill and three scripts that the next mark starts from.
What shipped is a vermilion seal bearing 桜 next to “oharu” set in Fraunces, plus JetBrains Mono on article pages only.
The blog had no design, and I had been living with it
Here is what the favicon was:
rect and one <text> element asking for
Helvetica,Arial,sans-serif, at the three sizes a favicon is used at. The
letter is whatever the visitor’s operating system supplied.The header had no mark at all, just the site title as text.
The typography was the same story:
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, …;--font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, …;Pure system stacks: there was no font. Every visitor saw whatever fonts their operating system shipped. That is a legitimate trade (zero bytes, zero layout shift), but it was not a decision anyone had made.
Wrong turn 1: twenty-four variations of one idea
I asked for breadth first: twenty-four concepts across icon-only, wordmark, and combination marks, rendered into a gallery showing each at 128px, 32px and 16px at once.
It produced twenty-four of them. They were all the same logo.

That is the icon-only third of the sheet. Aperture, orbit, terminal, arcs, torii, squircle, prompt, split. Eight names for one drawing.
Every one is a monoline geometric stroke: circles, arcs, even weight, rounded caps. Icon or wordmark, they share a skeleton, and that skeleton is the common default for generated logos. I had asked for range and received repetition wearing eight hats.
The fix was to look at real work first. Six identities, screenshotted and actually examined:
| Reference | What it does | What round one had done |
|---|---|---|
| Linear, Astro | Solid fills, mark small and quiet | All strokes, which close up at 16px |
| Rauno Freiberg, Josh Comeau | No mark at all. Type plus one committed colour | Assumed a mark was mandatory |
| Maggie Appleton | Display serif. It is a writing site | Geometric sans, the template default |
| Anthony Fu | Personal monogram, hand character | Pure geometry cannot produce “personal” |
Round two came back genuinely different, because this time the constraints came from real sites:

An AI asked for N options returns N samples from one distribution.
Wrong turn 2: letterforms that were never rendered
Hand-drawn SVG letterforms carry bugs that only rendering shows, and round one
went into a gallery without being rendered once. The worst was the letter u:
M9,49 L9,99 A41,41 0 0 0 91,99 L91,140Read it as a pen. Left stem down from the x-height to y=99. Bowl arc across to
(91,99). Then a line down to the baseline. The arc ends at y=99, so the
right stem only spans 99 to 140: it is missing its entire top half. The
letter renders as a u with a stunted right side.
The fix draws the right stem as its own subpath, at full x-height:
M9,49 L9,99 A41,41 0 0 0 91,99M91,49 L91,140
Two more bugs were the same class of error:
- A subtitle positioned at
y=160and scaled to0.26put its ascenders at160 + 0.26 × (−700) = −22, above the baseline of the word it sat under, sot,h,bandlpunched through the name. - A mark drawn 220 units tall next to letters 700 units tall read as a stray bullet point rather than a logo.
Every one of these is obvious in a browser and invisible in the path data.
Wrong turn 3: a character the agent assumed
The strongest concept to come out of round two was a hanko, a personal seal, which is close to what a blog actually is. Vermilion, one character, knocked out in white.
The name is “oharu”, so the agent used 春 (haru, spring). It looked good. It nearly got built.
Then it asked, because putting a character into someone’s identity on an assumption is not a thing to do quietly. I corrected it: “oharu” is 桜明.
That answer changed the design, not just a glyph. 桜明 is two characters, and two characters cannot survive a 16px favicon: at that size a 10-stroke kanji turns to texture.

The 16px renderings settle it. 桜明 stacked vertically is the correct hanko for the name and the worst performer in a browser tab; the outlined version keeps its frame and loses both characters. So the seal became a round, single-character 桜, and one character over two was settled by stroke count rather than taste.
Wrong turn 4: a bounding box computed with a regex
For the shipped assets, the hand-drawn letters gave way to outlines baked from Fraunces (wordmark) and Noto Serif JP (桜), using fontTools and HarfBuzz. Correct kerning and contrast, frozen into path data, so the wordmark ships as geometry with no webfont request and no flash of fallback type.
Placing a glyph needs its inked bounds. outline.py pulled every number out of
the path string and treated them as alternating x, y pairs. SVGPathPen emits
H and V shorthand, and those commands carry a single coordinate. One
horizontal line flips the parity of every number after it:
path M0,0 H700 L720,50real points (0,0) (700,0) (720,50)regex pairs (0,0) (700,720) (50, …)It survived galleries looking merely slightly off. It only became undeniable when a lockup that should have been roughly 3200 units wide came out at 1802.
The fix is to stop parsing text and let the library measure:
from fontTools.pens.boundsPen import BoundsPen
bounds = BoundsPen(glyph_set)for info, pos in zip(buf.glyph_infos, buf.glyph_positions): xform = (scale, 0, 0, -scale, (x + pos.x_offset) * scale, 0) glyph_set[name].draw(TransformPen(pen, xform)) glyph_set[name].draw(TransformPen(bounds, xform)) # same transformReal bounds for the wordmark: 2456.4 × 758.6. The regex had been confidently returning
something else.
It did not crash. It produced plausible output that a human glanced at and approved, twice.
Wrong turn 5: a preload on every page
Code blocks also got their own typeface. They are the surface a tech blog is actually read on, and mine were rendering in Menlo, the same “no decision was made” state as the body text.
JetBrains Mono Variable, Latin subset only, 40KB, self-hosted at a fixed path so it can be preloaded by URL. No CJK font is fetched for code: code is Latin, so the CJK stacks stay untouched.
The preload went into the shared layout. The browser said why that was wrong:
The resource http://localhost:4321/fonts/jetbrains-mono-latin.woff2 waspreloaded using link preload but not used within a few seconds from thewindow's load event. Please make sure it has an appropriate `as` value andit is preloaded intentionally.The home page, the tag listings and both CJK index pages have no code on them.
Each one downloaded 40KB and used none of it. Gating the preload behind a
preloadMono prop that only ArticleLayout sets:
| Page | rel="preload" links |
|---|---|
dist/index.html | 0 |
dist/ja/index.html | 0 |
dist/tags/index.html | 0 |
dist/blog/<article>/index.html | 1 |
One console warning, one real saving. This is the one wrong turn a tool reported on its own. Everything else surfaced only by rendering, measuring, or asking.
What the rounds became: a /logo skill
The skill’s non-negotiables, and where each one came from:
| Rule | Where it came from |
|---|---|
| Study real work before generating anything | Twenty-four concepts from one distribution |
| Render it and look before showing it to anyone | The u with half a stem |
| Judge at 16px | 桜明 as a smudge in a browser tab |
| Never bake in an assumption about the person | 春 |
| Use real typeface outlines, not hand-rolled letterforms | The hand-built geometry, and the regex that measured it |
| Decide colour by comparison, not theory | A blue wordmark beside a red seal |
| The user picks, at every gate | The loop itself |
The skill is told to present, recommend with reasons, and stop: an opinion is useful, a decision is not yours. Every round in this article ended at a gate where I chose. That is why five wrong turns cost an afternoon rather than a finished identity built on a guess.
Three scripts do the mechanical half:
gallery.mjsbuilds the contact sheet and serves it on a local port.outline.pybakes typeface outlines into path data through fontTools and HarfBuzz.build-brand-lockup.mjsis the worked example that emits the favicon and the header component.
Concepts and sheets are written to a scratch directory, never into the repo: a design round is mostly rejects, and a repository is not where rejects go.
What shipped
A vermilion round seal bearing 桜, next to “oharu” set in Fraunces. It ships as two SVGs rather than one. The seal is deliberately shorter than the wordmark’s ink height, so clipping the full lockup to a square cannot produce the narrow-screen variant.
Below 50rem the seal appears alone, which also solved an old layout problem: on a phone, the text wordmark competed with the search control for space.
One colour decision came from putting the options side by side. The site accent
is #1d4ed8; the seal is #d8452a. Those sit almost opposite each other, and a
blue wordmark beside a red seal visibly fights. The wordmark takes the theme’s
ink and blue stays on links, so the two never sit adjacent.
| Wordmark path data | 3569 bytes (integer-rounded from 5145) |
| Seal path data | 1677 bytes |
favicon.svg | 1878 bytes |
| JetBrains Mono, Latin subset | 40KB, article pages only |
Webfont requests on a /ja/ article | 1 (JetBrains Mono), no CJK font |
Summary
Five wrong turns. Not one of them was caught by reasoning about the code.
The generic concepts needed real references to become visible as generic. The broken letterforms needed a browser. The wrong kanji needed a question. The bounding box bug needed a number absurd enough to notice. The wasted preload needed a console.
These five are the argument for running this as a loop rather than as a prompt. Producing twenty-four polished concepts costs minutes now, which makes it easy to mistake volume for range and to accept a confident artefact built on a guess.
What a loop adds over a prompt is rendering, measuring, and asking when the design depends on a fact only I have.
None of the five felt like waste at the time, and none reads like waste now. The next mark starts at seven rules and three scripts instead of at a blank prompt.


