minpeter/minpeter.v2
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 23, 2026
Low
Design risk in this codebase.
More findings
Verdict
Minimal editorial polish sits on top of structural gaps: no page headings, a back-link that vanishes for external posts, and a type scale undercut by ad-hoc bracket values. The craft is real where it was applied, like the loading-state fallback shapes, but it never reached error states or navigation continuity.
Files Rams reviewed
app/[locale]/73e3da8fa7a397e7b1bc36efabb2cbb265524a75d7d5e6d1620b9e10e694257/page.tsx
app/[locale]/blog/[...slug]/page.tsx
app/[locale]/blog/page.tsx
app/[locale]/layout.tsx
app/[locale]/page.tsx
app/[locale]/resume/page.tsx
app/[locale]/show/dynamic-hacked-text/page.tsx
app/[locale]/show/model-card-artwork/page.tsx
app/[locale]/show/new-year-clock/page.tsx
app/[locale]/show/page.tsx
app/[locale]/show/tech-stack-ball/page.tsx
app/[locale]/show/unstructured-0828/page.tsx
app/[locale]/show/unstructured/page.tsx
app/[locale]/show/yet-another-tempfiles/page.tsx
components/not-found-page.tsx
app/[locale]/show/new-year-clock/layout.tsx
app/[locale]/blog/[...slug]/mdx-components.tsx
app/[locale]/blog/[...slug]/post-footer.tsx
app/[locale]/blog/[...slug]/post-toc.tsx
app/[locale]/blog/list-fallback.tsx
app/[locale]/blog/list.tsx
app/[locale]/blog/loading.tsx
app/[locale]/blog/og/[...slug]/route.tsx
app/[locale]/resume/loading.tsx
app/[locale]/show/dynamic-hacked-text/animated-text.tsx
app/[locale]/show/loading.tsx
app/[locale]/show/new-year-clock/countdown.tsx
app/[locale]/show/tech-stack-ball/animated-stack.tsx
app/[locale]/show/tech-stack-ball/playground-wrapper.tsx
app/[locale]/show/unstructured/styles.module.css
UX
app/[locale]/blog/[...slug]/page.tsx:87
External-linked posts skip the Header, losing the only 'back to blog' link
When `post.data.external_url` is set, the page returns only `<ExternalRedirect url={post.data.external_url} />` and short-circuits before `Header` (which carries the `backToBlog` link), `MachineTranslationNotice`, `PostToc`, and `PostFooter` ever render.
Why it matters
Anyone who lands on this route before or without the redirect firing (slow network, JS disabled, a bookmarked/shared link) has no in-page link back to the blog list and must rely on the browser's back button, which fails entirely if this was a direct entry.
Fix
Keep the page's navigation chrome mounted for every post, and render the redirect content inside it rather than replacing it.
if (post.data.external_url) {
return <ExternalRedirect url={post.data.external_url} />;
}if (post.data.external_url) {
return (
<section className={cn(styles.stagger_container, styles.post, "blog-post-page")}>
<Header
link={{ href: `/${locale}/blog` as Route, text: t("backToBlog") }}
title={post.data.title}
/>
<ExternalRedirect url={post.data.external_url} />
</section>
);
}app/[locale]/blog/page.tsx:75
Blog list has a loading fallback but no error state if the post fetch fails
`Suspense` around `BlogList` only defines a `fallback` (`BlogSearchShell` + `BlogListFallback`) for the loading state. There is no error boundary around it, so a thrown error from `blog.getPages`/`getPostsMetadata` or inside `BlogList` has no designed recovery UI.
Why it matters
If the content source fails to resolve, the page either crashes to a generic Next.js error screen or renders blank, and the user has no retry path: the loading state was designed, the failure state wasn't.
Fix
Wrap the Suspense boundary in an error boundary that renders a retry-capable error state.
<Suspense
fallback={
<>
<BlogSearchShell searchPlaceholder={t("searchPlaceholder")} />
<BlogListFallback lang={locale} posts={posts} />
</>
}
>
<BlogList lang={locale} posts={posts} />
</Suspense><ErrorBoundary fallback={<BlogListError />}>
<Suspense
fallback={
<>
<BlogSearchShell searchPlaceholder={t("searchPlaceholder")} />
<BlogListFallback lang={locale} posts={posts} />
</>
}
>
<BlogList lang={locale} posts={posts} />
</Suspense>
</ErrorBoundary>app/[locale]/blog/page.tsx:27
Document title stays hardcoded English while the description is fully localized
`generateMetadata` translates the description with `t("blogPageDescription")` for the resolved `locale`, but sets `title: "minpeter | blog"` as a fixed string with no translation lookup.
Why it matters
Non-English visitors get a browser tab, search result, and share-card title that never matches the language of the rest of the localized page, which reads as unfinished internationalization.
Fix
Route the title through the same translation function used for the description.
const baseMetadata = createMetadata({
description: t("blogPageDescription"),
locale,
path: "/blog",
title: "minpeter | blog",
});const baseMetadata = createMetadata({
description: t("blogPageDescription"),
locale,
path: "/blog",
title: t("blogPageTitle"),
});Accessibility
app/[locale]/page.tsx:41
Home logo link has a 32x32px tap target with no padding
The `Link` labeled by `t("homeLabel")` wraps a 32x32 `Image` and only carries `mb-6 inline-flex hover:opacity-60`, no padding to extend the clickable area beyond the icon itself.
Why it matters
A 32px target falls below the 44px touch minimum, making the primary home link harder to hit reliably on mobile, especially for users with motor impairments.
Fix
Extend the hit area with padding so the tappable region reaches 44x44px without resizing the visible mark.
<Link
aria-label={t("homeLabel")}
className="home-logo-link mb-6 inline-flex hover:opacity-60"
href={`/${locale}` as Route}
><Link
aria-label={t("homeLabel")}
className="home-logo-link mb-6 inline-flex p-2.5 -m-2.5 hover:opacity-60"
href={`/${locale}` as Route}
>Get this score on every PR.
Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.
Install Rams freeTypography
app/[locale]/page.tsx:56
Arbitrary bracket values on the h1 and intro paragraph bypass the type scale
The hero h1 uses `tracking-[-0.035em]` and the intro paragraph uses `text-[15px] leading-[1.35] tracking-[-0.02em]` instead of scale tokens. These are magic numbers with no named role in the system, so future edits to type spacing have no single source to change.
Why it matters
Each bracketed value is a one-off decision nobody can find or reuse elsewhere, so the type scale drifts wider every time a new page copies this pattern instead of reaching for a token.
Fix
Replace arbitrary bracket values with the nearest scale token (tracking-tight, text-sm, leading-snug).
<h1 className="text-base leading-tight tracking-[-0.035em]">
<span className="font-semibold text-foreground">MINPETER</span>
<span className="text-muted-foreground"> — {t("role")}</span>
</h1>
<p className="mt-4 max-w-md text-[15px] leading-[1.35] text-foreground/85 tracking-[-0.02em]">
{t("introduction")}
</p><h1 className="text-base leading-tight tracking-tight">
<span className="font-semibold text-foreground">MINPETER</span>
<span className="text-muted-foreground"> — {t("role")}</span>
</h1>
<p className="mt-4 max-w-md text-sm leading-snug text-foreground/85">
{t("introduction")}
</p>Spacing
app/[locale]/73e3da8fa7a397e7b1bc36efabb2cbb265524a75d7d5e6d1620b9e10e694257/page.tsx:29
Back link sits almost fused to the poem with only 4px of gap
The outer `<section>` uses `gap-1` (4px) between the `Backlink` wrapper and the poem's `<div>`. Navigation chrome and page content are unrelated elements but read as one glued block at this distance.
Why it matters
Proximity is the reader's first signal of relationship. Compressing the gap between 'back' navigation and the actual poem content flattens the page's structure, so the eye can't separate 'where I came from' from 'what I'm reading'.
Fix
Give unrelated blocks more separation than related lines within a block; increase the gap between the backlink and the poem group.
<section className="flex flex-col gap-1"><section className="flex flex-col gap-6">Color
Components
Motion
Craft
Working well
- The word-breaking setup, once untangled, is doing something genuinely thoughtful: `word-break: keep-all` prevents Korean prose from breaking mid-syllable, while `overflow-wrap: anywhere` is carved out specifically for `a`, `code`, `kbd`, and `samp` so long URLs or hashes still wrap instead of blowing out the layout. That's exactly the trade-off this content needs.
- Setting `lang="ko"` on the poem's wrapping div is exactly right: it tells screen readers and browsers to switch pronunciation and hyphenation rules for the Korean text, even though the surrounding UI (the 'back' link) is likely rendered in the site's default locale. This is the correct scoped-lang pattern rather than forcing lang on the whole document.
- Setting `lang={locale}` directly on the MDX content wrapper is the correct pattern: since blog posts can be machine-translated per post, scoping `lang` to the rendered content (rather than assuming it matches the root `<html lang>`) means screen readers pick the right pronunciation for translated posts specifically.
- The h1 gets its hierarchy from weight and color (`font-semibold text-foreground` for the name, `text-muted-foreground` for the role) rather than a separate larger size. That's a legitimate way to signal importance without inflating the type scale, and it stays consistent with the page's generally restrained sizing.
Scored July 23, 2026 with Rams Engine v0.0.2 · Engine changelog
Score your own repo.
Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.
Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.
Or get this review on every PR.