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.

Updated

The Claude starburst logo in terracotta and the Claude wordmark in black on a white card
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:

The old favicon at three sizesA rounded blue square with a white lowercase o centred in it, drawn at 128, 32 and 16 pixels. The letter stays legible at every size, and at 16 pixels the mark is a plain blue square with a small light shape in the middle.ooo128px32px16px
A rounded 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:

src/styles/global.css
--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.

Eight icon concepts in a grid, all drawn in the same heavy blue monoline
stroke with rounded ends: rings, arcs, a rounded square, a chevron

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:

ReferenceWhat it doesWhat round one had done
Linear, AstroSolid fills, mark small and quietAll strokes, which close up at 16px
Rauno Freiberg, Josh ComeauNo mark at all. Type plus one committed colourAssumed a mark was mandatory
Maggie AppletonDisplay serif. It is a writing siteGeometric sans, the template default
Anthony FuPersonal monogram, hand characterPure geometry cannot produce “personal”

Round two came back genuinely different, because this time the constraints came from real sites:

Sixteen concepts grouped as editorial serif, grotesk, vermilion hanko seal,
type with colour, and monogram. Visibly five different ideas rather than one

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:

u, as drawn
M9,49 L9,99 A41,41 0 0 0 91,99 L91,140

Read 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:

u, fixed
M9,49 L9,99 A41,41 0 0 0 91,99
M91,49 L91,140

The word oharu twice: above, the final u has a right stroke that stops at
half height; below, the same word with the stroke rising to full x-height like
its neighbours

Two more bugs were the same class of error:

  1. A subtitle positioned at y=160 and scaled to 0.26 put its ascenders at 160 + 0.26 × (−700) = −22, above the baseline of the word it sat under, so t, h, b and l punched through the name.
  2. 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.

Six vermilion seal options, each shown large and again at 32 and 16 pixels.
At 16 pixels the two-character versions are indistinct blocks while the
single-character round seal still reads

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,50
real 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:

outline.py
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 transform

Real 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 was
preloaded using link preload but not used within a few seconds from the
window's load event. Please make sure it has an appropriate `as` value and
it 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:

Pagerel="preload" links
dist/index.html0
dist/ja/index.html0
dist/tags/index.html0
dist/blog/<article>/index.html1

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:

RuleWhere it came from
Study real work before generating anythingTwenty-four concepts from one distribution
Render it and look before showing it to anyoneThe 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 letterformsThe hand-built geometry, and the regex that measured it
Decide colour by comparison, not theoryA blue wordmark beside a red seal
The user picks, at every gateThe 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:

  1. gallery.mjs builds the contact sheet and serves it on a local port.
  2. outline.py bakes typeface outlines into path data through fontTools and HarfBuzz.
  3. build-brand-lockup.mjs is 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 data3569 bytes (integer-rounded from 5145)
Seal path data1677 bytes
favicon.svg1878 bytes
JetBrains Mono, Latin subset40KB, article pages only
Webfont requests on a /ja/ article1 (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.

Share this article