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.
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.
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.
Generated 2h ago · anthropic/claude-sonnet-5-5 · with screenshots of 3 pages and 4 flagged values
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.
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.
- 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.
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.
Readers rage-click post headings, mistaking the link styling for a link.
Mark fixedIgnoreFile bugThe byline grey fails on paper.
fix suggestedThe 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
# 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 …
- 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.”
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.
- 01Polishd
Create fix
Labels the issue and comments the brief, opening with
@claude.fix requested - 02GitHub Actions
The comment starts a run
Your
claude.ymlhearsissue_commentand runs the Claude Code Action on your runners.agent working - 03GitHub Actions
Scope, confirm, fix
Claude checks the bug is real and fits one pull request, then pushes a branch.
fix ready - 04Polishd
Create PR
Opens the pull request from that branch,
Fixes #42first. Only when you click.PR open - 05You
Review and merge
Merging closes #42 on GitHub, and the row reads it back.
fix merged
Claude Code · #42
Triggered via issue_commentfieldnote commented on #426m 31s
- Set up job2s
- Checkout repository1suses: actions/checkout@v4
- Run Claude Code6m 4suses: anthropics/claude-code-action@v1
Polishd's brief — it stops, changing nothing, at the first step that fails
- Scope it —
components/narration-button.tsx - Confirm it's real — play() runs on mount, before any tap
- Check it's feasible — one file, no new dependencies
- Fix it — pushed
claude/issue-42-20261025-1803
- Scope it —
- Complete job0s
# .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
#42 on GitHubPR #43fix requested 38m ago
Pull request #43 is open, opened by fieldnote. It's linked: merging it closes this issue.
#43 fix: start narration on the first tap
fieldnote · closes this issue · updated 3m ago · claude/issue-42-20261025-1803
- fieldnote linked pull request #43 · 3m ago
- claude commentedagent · 31m ago
Claude finished @fieldnote's task in 6m 4s — View job · claude/issue-42-20261025-1803 · Create PR ➔
- 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.
- 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.
Watch what people actually do.
Rage clicks, dead clicks, scroll depth, web vitals — captured from real sessions, into your own database. Anonymous by design.
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.
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.
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.
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.
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.
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.
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")
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.
Install
One package. Next.js 15+, App Router.
$ npm i @polishd/nextInit
Wires the proxy, client, route and dashboard for you.
$ npx polishd initLook
Run the app and open the dashboard.
$ npm run dev # → /polishd