The taste loop for Next.js

Build fast, refine relentlessly.

Vibe coding gets you to a working build in a weekend, and hands the rough edges to your users to discover. Shipping got cheap; finishing didn't. Polishd reads back the design your site really ships, watches how it's really used, and closes the loop on fixing both. Because the taste is in the details.

$ npm i @polishd/next
Read the docs

Live today · the design tab

You already have a design system.
Nobody ever wrote it down.

Forty files of generated components, and somewhere in them a type scale, a palette and a spacing rhythm that nobody chose on purpose. Polishd reads it back out of your rendered pages and prints the brand guide you never made — after the build, not before.

Color palette
7 pages scanned
Primary
Rust
#c2410c
Secondary
White
#ffffff
Accent
Teal
#0f766e
Neutral 100
#f5f1ea
Neutral 200
#e7e2d8
Neutral 400
#a8a29e
Neutral 500
#78716c
Neutral 600
#57534e
Neutral 900
#1c1917
#c4400d sits one step from #c2410c and covers 1% of the UI, so it isn't on the board. One of the two is a typo nobody has noticed.
Typography
16 sizes
Aa44px / 700×12
Aa28px / 400×31
Aa21px / 500×3
Aa17px / 400×388
Aa15px / 400×6
Aa14px / 500×96
Aa13px / 500×205
+9 more
Sixteen sizes across seven pages, nine of them used fewer than five times. Those aren't steps in a scale — they're accidents with a font-size.
Corner radii6 radii
4px×41
6px×18
8px×27
12px×9
14px×2
full×12
6 distinct corner radii (4px, 6px, 8px, 12px, 14px, full) — most systems settle on 2–4 (e.g. small, medium, full).
Contrast2 pairs fail AA
Aa#a8a29e on #ffffff31%needs 49% at 13px
Aa#78716c on #f5f1ea48%needs 49% at 17px
A hair under the line, on your body text. Nobody eyeballs that.
Aesthetic review

One serif for display and one rust for every action: this reads as a single publication. What breaks it is the long tail — sizes and greys that appear once, doing a job an existing token already does, and a byline grey (#a8a29e on #ffffff, 31% contrast) that costs readers on every post.

3 problems from the design review to act onActionables

Generated 2h ago · anthropic/claude-sonnet-5-5 · with screenshots of 3 pages and 4 flagged values

Spacing30% off-grid
8px
×41
12px
×26
13px
×4off-grid
16px
×58
18px
×16off-grid
22px
×9off-grid
30% of padding values sit off a 4px grid (13px, 18px, 22px) — consistent rhythm usually comes from a 4px or 8px base unit.

Every number above is measured, not guessed — each page tallies its own rendered CSS once per visitor session, and the flags are arithmetic. The model gets the last word, on a tally it can't argue with, and it sees the pages too: a review shoots the first screen of the top pages and pictures each flagged value in place.

Live today · stages 01–04

What gets measured,
gets improved.

Refining needs evidence. This is the captured half of the loop — no surveys, no session-replay creep, running on your own database, on a dashboard at /polishd.

Sessions
1,253
Page views
4,850
Rage clicks
53
Dead clicks
1,025
Events
19,541
Device sizes1,253 sessions
Mobile86.8%
Desktop11.3%
Tablet1.9%
Built for desktop. Read on mobile. That's a finding, not a number.
All you need to know

Designed as a desktop-first reading experience, yet 86.8% of visitors arrive on mobile. Attention is excellent — long reads hold for minutes — but people keep tapping static text as though it were a link, and one article records no scroll at all.

Wins
  • Deep engagement on long reads — an average dwell of nearly 32 minutes.
  • Strong adoption of the narration feature: 254 clicks across 6 pages.
  • Core articles scrolled clean through to 100% completion.
4 problems from the analytics to act onActionables
Generated 2h ago · anthropic/claude-sonnet-5-5 · refreshes daily
3 components have appeared since the last codebase scan.Re-scan in Settings

Live today · actionables

A bug, not a chart.
File it, fix it, or hand it off.

Both reviews hand what they find to one list: the analytics losses and the design issues together, each with its evidence and, for a design value, a picture of where it renders. File one and it's verified against your source first. Mark one fixed, or ignore it with a reason the model reads back.

To act on
2
Filed on GitHub
2
Open there
1
Closed
1
Actionables
fieldnote/slow-web
  • Readers rage-click post headings, mistaking the link styling for a link.

    Mark fixedIgnoreFile bug
  • The byline grey fails on paper.

    fix suggested
  • The narration button throws on Safari: play() rejected without a gesture.

    agent working

    #42 on GitHubopened 2h ago · 2 comments

  • A 21px subheading used three times stands in for the 28px step.

    fix merged

    #39 on GitHubopened 3d ago · PR #40 merged yesterday

  • Two rusts a hair apart split the accent.
    “Collapsed #c4400d into the --accent token.”Edit noteUndo
  • Zero recorded scroll depth on one article — likely a viewport or script failure.
    “The page is an audio essay; its scroll container is the player.”Edit reasonUndo
Filed issues go on to your coding agent, and come back as pull requests.Create fix
Fix promptCopy fix prompt7
# Fix the analytics and design problems Polishd found

**A1.** Readers rage-click post headings, mistaking the link styling for a link.
  - Evidence: `h2.post-title` · 53 rage clicks · 31 sessions
  - Source: `app/posts/[slug]/page.tsx`

**D1.** The byline grey fails on paper.
  - Evidence: `#a8a29e` on `#ffffff` at 13px (2.6:1)
  - Where it renders: `#a8a29e` — 212 uses as text on 7 pages (pictured on /posts/the-slow-web)
  - Suggested fix: Use `#57534e` for captions (7.4:1).
…

Finish with a fenced JSON verdict block, one entry for each of A1, A2, D1 …
Paste verdicts3 of 7 answered
  • A1fixedReaders rage-click post headings, mistaking the link styling for a link.Nothing to record — the next review shows whether the fix held.
  • A2not realZero recorded scroll depth on one article — likely a viewport or script failure.Will be ignored — “The page is an audio essay; its scroll container is the player.”
  • D1deliberateTwo rusts a hair apart split the accent.Will be ignored — “#c4400d is the pressed state of the primary button.”
Ignore 2 findings

A citation that isn't in the data is thrown away before anything is filed. Marking a finding fixed tells the model it was fixed, and on which day, so data from before the fix doesn't bring it back. A pasted verdict only ever matches the finding it was given.

Just landed · fixes

Issue in, PR out.
On the GitHub Actions you already run.

Create fix hands a filed issue to the coding agent your repo already runs — the Claude Code GitHub Action. It scopes the bug, proves it's real and pushes a fix; Polishd opens the pull request, linked to the issue, and reads the agent's verdict back onto the row.

Create fix, end to end
Live now
  1. 01Polishd

    Create fix

    Labels the issue and comments the brief, opening with @claude.

    fix requested
  2. 02GitHub Actions

    The comment starts a run

    Your claude.yml hears issue_comment and runs the Claude Code Action on your runners.

    agent working
  3. 03GitHub Actions

    Scope, confirm, fix

    Claude checks the bug is real and fits one pull request, then pushes a branch.

    fix ready
  4. 04Polishd

    Create PR

    Opens the pull request from that branch, Fixes #42 first. Only when you click.

    PR open
  5. 05You

    Review and merge

    Merging closes #42 on GitHub, and the row reads it back.

    fix merged
GitHub Actions
Success

Claude Code · #42

Triggered via issue_commentfieldnote commented on #426m 31s

Job · claudeubuntu-latest
  1. Set up job2s
  2. Checkout repository1s
    uses: actions/checkout@v4
  3. Run Claude Code6m 4s
    uses: anthropics/claude-code-action@v1

    Polishd's brief — it stops, changing nothing, at the first step that fails

    1. Scope it — components/narration-button.tsx
    2. Confirm it's real — play() runs on mount, before any tap
    3. Check it's feasible — one file, no new dependencies
    4. Fix it — pushed claude/issue-42-20261025-1803
  4. Complete job0s
The workflow it heardread, never written
# .github/workflows/claude.yml
on:
  issue_comment:
    types: [created]
jobs:
  claude:
    if: contains(github.event.comment.body, '@claude')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anthropics/claude-code-action@v1
The narration button throws on Safari
PR open

#42 on GitHubPR #43fix requested 38m ago

Fix and pull requests

Pull request #43 is open, opened by fieldnote. It's linked: merging it closes this issue.

Claude's verdict fixed — Narration now starts inside the tap handler; Safari rejects play() outside a user gesture.components/narration-button.tsxcomponents/narration-button.test.tsx
open

#43 fix: start narration on the first tap

fieldnote · closes this issue · updated 3m ago · claude/issue-42-20261025-1803

Activity on GitHub
  1. fieldnote linked pull request #43 · 3m ago
  2. claude commentedagent · 31m ago

    Claude finished @fieldnote's task in 6m 4s — View job · claude/issue-42-20261025-1803 · Create PR ➔

  3. fieldnote asked for a fixpolishd · 38m ago

    @claude polishd is asking you to fix #42 — but only once you've confirmed it's real and can be fixed in one focused pull request.

  4. fieldnote added polishd-fix · 38m ago

Polishd runs nothing itself: the agent runs in your repository, on your Actions minutes and your Claude plan or API key, with the token you already gave Polishd — no new permissions. Copilot's coding agent works too, assigned instead of mentioned. A bug that isn't real comes back as a verdict, not a diff, and nothing is written to a pull request without your click.

The loop

Every rough edge,
from signal to shipped fix.

Seven stages, one cycle. Five of them are live now, fixes included — the last two are coming soon.

TRACKMEASUREANALYSESURFACEFIXEXPERIMENTVERIFY
Stage
01
Track
Live now
01TrackLive now

Watch what people actually do.

Rage clicks, dead clicks, scroll depth, web vitals — captured from real sessions, into your own database. Anonymous by design.

rage clickdead clickscroll depthweb vitals
02MeasureLive now

Turn a feeling into a number.

Events roll up per page, per component, per session — so “this page feels off” becomes a figure you can watch move.

03AnalyseLive now

What the numbers actually mean.

A model reads a compact digest of the aggregates and writes the plain-English story of how your site is really being used.

04SurfaceLive now

A bug, not a chart.

Every loss and design issue lands in one Actionables list, cited to the exact page, selector or source file — verified against your source and filed to GitHub in one click. Citations that aren’t in the data are thrown away.

05FixLive now

Hand it to your coding agent.

Create fix briefs the Claude Code GitHub Action on the filed issue. It proves the bug is real, pushes a fix, and comes back as a pull request linked to the issue — or as a reason it changed nothing.

github actionsclaude-code-actionFixes #Nverdict
06ExperimentComing soon

Ship it behind a flag.

Roll the fix out to a slice of traffic and let the loop hold both versions side by side, measured the same way.

07VerifyComing soon

Did the signal actually move?

If yes, deploy it. If not, the loop runs again — and keeps running until the answer is yes.

↺

Deploy if the answer is yes. Otherwise, back to stage 01.

Next · stages 06–07

The half that closes the loop.

Finding the rough edge is only worth something if the fix is proven. These two stages are in build — what's live now is the first five.

06ExperimentComing soon

Ship it behind a flag.

Roll the fix out to a slice of traffic. Both versions stay live, measured by the same signals that found the problem.

flag polishd.experiment("tappable-headings")
07VerifyComing soon

Did the signal actually move?

Rage clicks down, scroll depth up — or not. Deploy on a yes; run the loop again on a no.

rage 53 → 7   verdict ship

Shapes shown are illustrative — the API isn't settled yet.

Start the loop

Three commands.
Then your own dashboard.

No third-party service, no script tag, no account. Events land in your database — SQLite locally, Postgres in production.

01

Install

One package. Next.js 15+, App Router.

$ npm i @polishd/next
02

Init

Wires the proxy, client, route and dashboard for you.

$ npx polishd init
03

Look

Run the app and open the dashboard.

$ npm run dev
# → /polishd

Make your website look polishd.

Live from the first signal to the pull request. The rest is coming soon, in all its glory.
$ npm i @polishd/next
Star on GitHub